From 59d55c222ca48403cd5923903f2b9cb7f1eaf37b Mon Sep 17 00:00:00 2001 From: Somebody Date: Sun, 25 Feb 2024 16:45:05 +0800 Subject: [PATCH] Implement packet handler sender --- Common/Configuration/ConfigContainer.cs | 2 +- Common/Util/Crypto.cs | 2 +- Common/Util/Extensions.cs | 70 + Common/Util/Logger.cs | 6 + GameServer/Enums/SessionState.cs | 11 + GameServer/Game/Player/Player.cs | 16 + GameServer/GameServer.csproj | 25 +- GameServer/KcpSharp/ArrayMemoryOwner.cs | 22 + GameServer/KcpSharp/AsyncAutoResetEvent.cs | 117 ++ .../DefaultArrayPoolBufferAllocator.cs | 13 + GameServer/KcpSharp/IKcpBufferPool.cs | 15 + GameServer/KcpSharp/IKcpConversation.cs | 23 + ...KcpConversationUpdateNotificationSource.cs | 8 + GameServer/KcpSharp/IKcpExceptionProducer.cs | 16 + .../KcpSharp/IKcpMultiplexConnection.cs | 58 + .../KcpSharp/IKcpMultiplexConnectionOfT.cs | 54 + GameServer/KcpSharp/IKcpTransport.cs | 19 + GameServer/KcpSharp/IKcpTransportOfT.cs | 22 + GameServer/KcpSharp/KcpAcknowledgeList.cs | 104 ++ GameServer/KcpSharp/KcpBuffer.cs | 59 + .../KcpSharp/KcpBufferPoolRentOptions.cs | 41 + GameServer/KcpSharp/KcpCommand.cs | 10 + ...KcpConversation.FlushAsyncMethodBuilder.cs | 275 ++++ GameServer/KcpSharp/KcpConversation.cs | 1399 +++++++++++++++++ GameServer/KcpSharp/KcpConversationOptions.cs | 99 ++ .../KcpSharp/KcpConversationReceiveResult.cs | 61 + .../KcpConversationUpdateActivation.cs | 490 ++++++ .../KcpConversationUpdateNotification.cs | 30 + .../KcpExceptionProducerExtensions.cs | 134 ++ GameServer/KcpSharp/KcpGlobalVars.cs | 14 + GameServer/KcpSharp/KcpKeepAliveOptions.cs | 30 + GameServer/KcpSharp/KcpMultiplexConnection.cs | 335 ++++ GameServer/KcpSharp/KcpPacketHeader.cs | 75 + GameServer/KcpSharp/KcpProbeType.cs | 10 + GameServer/KcpSharp/KcpRawChannel.cs | 372 +++++ GameServer/KcpSharp/KcpRawChannelOptions.cs | 33 + GameServer/KcpSharp/KcpRawReceiveQueue.cs | 358 +++++ GameServer/KcpSharp/KcpRawSendOperation.cs | 182 +++ GameServer/KcpSharp/KcpReceiveQueue.cs | 698 ++++++++ .../KcpReceiveWindowNotificationOptions.cs | 37 + GameServer/KcpSharp/KcpRentedBuffer.cs | 222 +++ GameServer/KcpSharp/KcpSendQueue.cs | 718 +++++++++ .../KcpSharp/KcpSendReceiveBufferItem.cs | 9 + .../KcpSharp/KcpSendReceiveBufferItemCache.cs | 73 + .../KcpSharp/KcpSendReceiveQueueItemCache.cs | 84 + GameServer/KcpSharp/KcpSendSegmentStats.cs | 20 + GameServer/KcpSharp/KcpSocketTransport.cs | 159 ++ .../KcpSocketTransportForConversation.cs | 46 + ...cpSocketTransportForMultiplexConnection.cs | 40 + .../KcpSocketTransportForRawChannel.cs | 41 + GameServer/KcpSharp/KcpSocketTransportOfT.cs | 222 +++ GameServer/KcpSharp/KcpStream.cs | 168 ++ .../AwaitableSocketAsyncEventArgs.cs | 36 + .../NetstandardShim/CancellationTokenShim.cs | 13 + .../NetstandardShim/LinkedListNetstandard.cs | 213 +++ .../NetstandardShim/TaskCompletionSource.cs | 11 + GameServer/KcpSharp/ThrowHelper.cs | 66 + GameServer/Program/EntryPoint.cs | 11 +- GameServer/Server/Connection.cs | 235 +++ GameServer/Server/GameSession.cs | 6 + GameServer/Server/Listener.cs | 138 ++ GameServer/Server/Packet/BasePacket.cs | 18 + GameServer/Server/Packet/CmdId.cs | 1164 ++++++++++++++ GameServer/Server/Packet/Handler.cs | 13 + GameServer/Server/Packet/HandlerManager.cs | 40 + GameServer/Server/Packet/Opcode.cs | 14 + .../Recv/Player/HandlerPlayerGetTokenCsReq.cs | 20 + WebServer/Handler/QueryGatewayHandler.cs | 8 +- 68 files changed, 9143 insertions(+), 10 deletions(-) create mode 100644 Common/Util/Extensions.cs create mode 100644 GameServer/Enums/SessionState.cs create mode 100644 GameServer/Game/Player/Player.cs create mode 100644 GameServer/KcpSharp/ArrayMemoryOwner.cs create mode 100644 GameServer/KcpSharp/AsyncAutoResetEvent.cs create mode 100644 GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs create mode 100644 GameServer/KcpSharp/IKcpBufferPool.cs create mode 100644 GameServer/KcpSharp/IKcpConversation.cs create mode 100644 GameServer/KcpSharp/IKcpConversationUpdateNotificationSource.cs create mode 100644 GameServer/KcpSharp/IKcpExceptionProducer.cs create mode 100644 GameServer/KcpSharp/IKcpMultiplexConnection.cs create mode 100644 GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs create mode 100644 GameServer/KcpSharp/IKcpTransport.cs create mode 100644 GameServer/KcpSharp/IKcpTransportOfT.cs create mode 100644 GameServer/KcpSharp/KcpAcknowledgeList.cs create mode 100644 GameServer/KcpSharp/KcpBuffer.cs create mode 100644 GameServer/KcpSharp/KcpBufferPoolRentOptions.cs create mode 100644 GameServer/KcpSharp/KcpCommand.cs create mode 100644 GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs create mode 100644 GameServer/KcpSharp/KcpConversation.cs create mode 100644 GameServer/KcpSharp/KcpConversationOptions.cs create mode 100644 GameServer/KcpSharp/KcpConversationReceiveResult.cs create mode 100644 GameServer/KcpSharp/KcpConversationUpdateActivation.cs create mode 100644 GameServer/KcpSharp/KcpConversationUpdateNotification.cs create mode 100644 GameServer/KcpSharp/KcpExceptionProducerExtensions.cs create mode 100644 GameServer/KcpSharp/KcpGlobalVars.cs create mode 100644 GameServer/KcpSharp/KcpKeepAliveOptions.cs create mode 100644 GameServer/KcpSharp/KcpMultiplexConnection.cs create mode 100644 GameServer/KcpSharp/KcpPacketHeader.cs create mode 100644 GameServer/KcpSharp/KcpProbeType.cs create mode 100644 GameServer/KcpSharp/KcpRawChannel.cs create mode 100644 GameServer/KcpSharp/KcpRawChannelOptions.cs create mode 100644 GameServer/KcpSharp/KcpRawReceiveQueue.cs create mode 100644 GameServer/KcpSharp/KcpRawSendOperation.cs create mode 100644 GameServer/KcpSharp/KcpReceiveQueue.cs create mode 100644 GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs create mode 100644 GameServer/KcpSharp/KcpRentedBuffer.cs create mode 100644 GameServer/KcpSharp/KcpSendQueue.cs create mode 100644 GameServer/KcpSharp/KcpSendReceiveBufferItem.cs create mode 100644 GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs create mode 100644 GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs create mode 100644 GameServer/KcpSharp/KcpSendSegmentStats.cs create mode 100644 GameServer/KcpSharp/KcpSocketTransport.cs create mode 100644 GameServer/KcpSharp/KcpSocketTransportForConversation.cs create mode 100644 GameServer/KcpSharp/KcpSocketTransportForMultiplexConnection.cs create mode 100644 GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs create mode 100644 GameServer/KcpSharp/KcpSocketTransportOfT.cs create mode 100644 GameServer/KcpSharp/KcpStream.cs create mode 100644 GameServer/KcpSharp/NetstandardShim/AwaitableSocketAsyncEventArgs.cs create mode 100644 GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs create mode 100644 GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs create mode 100644 GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs create mode 100644 GameServer/KcpSharp/ThrowHelper.cs create mode 100644 GameServer/Server/Connection.cs create mode 100644 GameServer/Server/GameSession.cs create mode 100644 GameServer/Server/Listener.cs create mode 100644 GameServer/Server/Packet/BasePacket.cs create mode 100644 GameServer/Server/Packet/CmdId.cs create mode 100644 GameServer/Server/Packet/Handler.cs create mode 100644 GameServer/Server/Packet/HandlerManager.cs create mode 100644 GameServer/Server/Packet/Opcode.cs create mode 100644 GameServer/Server/Packet/Recv/Player/HandlerPlayerGetTokenCsReq.cs diff --git a/Common/Configuration/ConfigContainer.cs b/Common/Configuration/ConfigContainer.cs index 6e7efa95..2a72efc9 100644 --- a/Common/Configuration/ConfigContainer.cs +++ b/Common/Configuration/ConfigContainer.cs @@ -37,7 +37,7 @@ namespace EggLink.DanhengServer.Configuration public string BindAddress { get; set; } = "127.0.0.1"; public int Port { get; set; } = 23301; public string PublicAddress { get; set; } = "127.0.0.1"; - public int PublicPort { get; set; } = 23301; + public uint PublicPort { get; set; } = 23301; public string GameServerId { get; set; } = "dan_heng"; public string GameServerName { get; set; } = "DanhengServer"; public string GameServerDescription { get; set; } = "A re-implementation of StarRail server"; diff --git a/Common/Util/Crypto.cs b/Common/Util/Crypto.cs index 01ad505f..f859ab77 100644 --- a/Common/Util/Crypto.cs +++ b/Common/Util/Crypto.cs @@ -8,7 +8,7 @@ using System.Threading.Tasks; namespace EggLink.DanhengServer.Util { - internal class Crypto + public class Crypto { private static Random secureRandom = new(); public static Logger logger = new("Crypto"); diff --git a/Common/Util/Extensions.cs b/Common/Util/Extensions.cs new file mode 100644 index 00000000..e8a81d7a --- /dev/null +++ b/Common/Util/Extensions.cs @@ -0,0 +1,70 @@ +using System.Buffers.Binary; + +namespace EggLink.DanhengServer.Util; + +public static class Extensions +{ + public static string JoinFormat(this IEnumerable list, string separator, + string formatString) + { + formatString = string.IsNullOrWhiteSpace(formatString) ? "{0}" : formatString; + return string.Join(separator, + list.Select(item => string.Format(formatString, item))); + } + public static void WriteConvID(this BinaryWriter bw, long convId) + { + //bw.Write(convId); + bw.Write((int)(convId >> 32)); + bw.Write((int)(convId & 0xFFFFFFFF)); + } + + public static long GetNextAvailableIndex(this SortedList sortedList) + { + long key = 1; + long count = sortedList.Count; + long counter = 0; + do + { + if (count == 0) break; + long nextKeyInList = sortedList.Keys.ElementAt((Index)counter++); + if (key != nextKeyInList) break; + key = nextKeyInList + 1; + } while (count != 1 && counter != count && key == sortedList.Keys.ElementAt((Index)counter)); + return key; + } + + public static long AddNext(this SortedList sortedList, T item) + { + long key = sortedList.GetNextAvailableIndex(); + sortedList.Add(key, item); + return key; + } + public static int ReadInt32BE(this BinaryReader br) => BinaryPrimitives.ReadInt32BigEndian(br.ReadBytes(sizeof(int))); + public static uint ReadUInt32BE(this BinaryReader br) => BinaryPrimitives.ReadUInt32BigEndian(br.ReadBytes(sizeof(uint))); + public static ushort ReadUInt16BE(this BinaryReader br) => BinaryPrimitives.ReadUInt16BigEndian(br.ReadBytes(sizeof(ushort))); + public static void WriteUInt16BE(this BinaryWriter bw, ushort value) + { + Span data = stackalloc byte[sizeof(ushort)]; + BinaryPrimitives.WriteUInt16BigEndian(data, value); + bw.Write(data); + } + public static void WriteInt32BE(this BinaryWriter bw, int value) + { + Span data = stackalloc byte[sizeof(int)]; + BinaryPrimitives.WriteInt32BigEndian(data, value); + bw.Write(data); + } + public static void WriteUInt32BE(this BinaryWriter bw, uint value) + { + Span data = stackalloc byte[sizeof(uint)]; + BinaryPrimitives.WriteUInt32BigEndian(data, value); + bw.Write(data); + } + + public static void WriteUInt64BE(this BinaryWriter bw, ulong value) + { + Span data = stackalloc byte[sizeof(ulong)]; + BinaryPrimitives.WriteUInt64BigEndian(data, value); + bw.Write(data); + } +} diff --git a/Common/Util/Logger.cs b/Common/Util/Logger.cs index 9d8f3c8e..5efbd8d8 100644 --- a/Common/Util/Logger.cs +++ b/Common/Util/Logger.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Text; @@ -105,6 +106,11 @@ namespace EggLink.DanhengServer.Util sw.WriteLine(message); } } + + public static Logger GetByClassName() + { + return new Logger(new StackTrace().GetFrame(1).GetMethod().ReflectedType.Name); + } } public enum LoggerLevel diff --git a/GameServer/Enums/SessionState.cs b/GameServer/Enums/SessionState.cs new file mode 100644 index 00000000..4414cfd2 --- /dev/null +++ b/GameServer/Enums/SessionState.cs @@ -0,0 +1,11 @@ +namespace EggLink.DanhengServer.Enums +{ + public enum SessionState + { + INACTIVE, + WAITING_FOR_TOKEN, + WAITING_FOR_LOGIN, + PICKING_CHARACTER, + ACTIVE + } +} diff --git a/GameServer/Game/Player/Player.cs b/GameServer/Game/Player/Player.cs new file mode 100644 index 00000000..1ac1afec --- /dev/null +++ b/GameServer/Game/Player/Player.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Game.Player +{ + public class Player + { + public async Task OnLogoutAsync() + { + + } + } +} diff --git a/GameServer/GameServer.csproj b/GameServer/GameServer.csproj index 1c6ed6d9..12e34365 100644 --- a/GameServer/GameServer.csproj +++ b/GameServer/GameServer.csproj @@ -15,8 +15,29 @@ - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GameServer/KcpSharp/ArrayMemoryOwner.cs b/GameServer/KcpSharp/ArrayMemoryOwner.cs new file mode 100644 index 00000000..ac3e690d --- /dev/null +++ b/GameServer/KcpSharp/ArrayMemoryOwner.cs @@ -0,0 +1,22 @@ +#if !NEED_POH_SHIM + +using System.Buffers; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class ArrayMemoryOwner : IMemoryOwner + { + private readonly byte[] _buffer; + + public ArrayMemoryOwner(byte[] buffer) + { + _buffer = buffer ?? throw new ArgumentNullException(nameof(buffer)); + } + + public Memory Memory => _buffer; + + public void Dispose() { } + } +} + +#endif diff --git a/GameServer/KcpSharp/AsyncAutoResetEvent.cs b/GameServer/KcpSharp/AsyncAutoResetEvent.cs new file mode 100644 index 00000000..e882d8f9 --- /dev/null +++ b/GameServer/KcpSharp/AsyncAutoResetEvent.cs @@ -0,0 +1,117 @@ +using System.Diagnostics; +using System.Threading.Tasks.Sources; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal class AsyncAutoResetEvent : IValueTaskSource + { + private ManualResetValueTaskSourceCore _rvtsc; + private SpinLock _lock; + private bool _isSet; + private bool _activeWait; + private bool _signaled; + + private T? _value; + + public AsyncAutoResetEvent() + { + _rvtsc = new ManualResetValueTaskSourceCore() + { + RunContinuationsAsynchronously = true + }; + _lock = new SpinLock(); + } + + T IValueTaskSource.GetResult(short token) + { + try + { + return _rvtsc.GetResult(token); + } + finally + { + _rvtsc.Reset(); + + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + _activeWait = false; + _signaled = false; + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _rvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => _rvtsc.OnCompleted(continuation, state, token, flags); + + public ValueTask WaitAsync() + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_activeWait) + { + return new ValueTask(Task.FromException(new InvalidOperationException("Another thread is already waiting."))); + } + if (_isSet) + { + _isSet = false; + T value = _value!; + _value = default; + return new ValueTask(value); + } + + _activeWait = true; + Debug.Assert(!_signaled); + + return new ValueTask(this, _rvtsc.Version); + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + + public void Set(T value) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if (_activeWait && !_signaled) + { + _signaled = true; + _rvtsc.SetResult(value); + return; + } + + _isSet = true; + _value = value; + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + } + +} diff --git a/GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs b/GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs new file mode 100644 index 00000000..0707adc8 --- /dev/null +++ b/GameServer/KcpSharp/DefaultArrayPoolBufferAllocator.cs @@ -0,0 +1,13 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class DefaultArrayPoolBufferAllocator : IKcpBufferPool + { + public static DefaultArrayPoolBufferAllocator Default { get; } = new(); + + public KcpRentedBuffer Rent(KcpBufferPoolRentOptions options) + { + return KcpRentedBuffer.FromSharedArrayPool(options.Size); + } + } + +} diff --git a/GameServer/KcpSharp/IKcpBufferPool.cs b/GameServer/KcpSharp/IKcpBufferPool.cs new file mode 100644 index 00000000..953cb94c --- /dev/null +++ b/GameServer/KcpSharp/IKcpBufferPool.cs @@ -0,0 +1,15 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// The buffer pool to rent buffers from. + /// + public interface IKcpBufferPool + { + /// + /// Rent a buffer using the specified options. + /// + /// The options used to rent this buffer. + /// + KcpRentedBuffer Rent(KcpBufferPoolRentOptions options); + } +} diff --git a/GameServer/KcpSharp/IKcpConversation.cs b/GameServer/KcpSharp/IKcpConversation.cs new file mode 100644 index 00000000..261b1b3a --- /dev/null +++ b/GameServer/KcpSharp/IKcpConversation.cs @@ -0,0 +1,23 @@ +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// A conversation or a channel over the transport. + /// + public interface IKcpConversation : IDisposable + { + /// + /// Put message into the receive queue of the channel. + /// + /// The packet content with the optional conversation ID. This buffer should not contain space for pre-buffer and post-buffer. + /// The token to cancel this operation. + /// A that completes when the packet is put into the receive queue. + ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken); + + /// + /// Mark the underlying transport as closed. Abort all active send or receive operations. + /// + void SetTransportClosed(); + } +} diff --git a/GameServer/KcpSharp/IKcpConversationUpdateNotificationSource.cs b/GameServer/KcpSharp/IKcpConversationUpdateNotificationSource.cs new file mode 100644 index 00000000..880ef8d4 --- /dev/null +++ b/GameServer/KcpSharp/IKcpConversationUpdateNotificationSource.cs @@ -0,0 +1,8 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal interface IKcpConversationUpdateNotificationSource + { + ReadOnlyMemory Packet { get; } + void Release(); + } +} diff --git a/GameServer/KcpSharp/IKcpExceptionProducer.cs b/GameServer/KcpSharp/IKcpExceptionProducer.cs new file mode 100644 index 00000000..79aab8a2 --- /dev/null +++ b/GameServer/KcpSharp/IKcpExceptionProducer.cs @@ -0,0 +1,16 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// An instance that can produce exceptions in background jobs. + /// + /// The type of the instance. + public interface IKcpExceptionProducer + { + /// + /// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation. + /// + /// The exception handler. + /// The state object to pass into the exception handler. + void SetExceptionHandler(Func handler, object? state); + } +} diff --git a/GameServer/KcpSharp/IKcpMultiplexConnection.cs b/GameServer/KcpSharp/IKcpMultiplexConnection.cs new file mode 100644 index 00000000..fe58be96 --- /dev/null +++ b/GameServer/KcpSharp/IKcpMultiplexConnection.cs @@ -0,0 +1,58 @@ +using KcpSharp; +using System.Net; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Multiplex many channels or conversations over the same transport. + /// + public interface IKcpMultiplexConnection : IDisposable + { + /// + /// Determine whether the multiplex connection contains a conversation with the specified id. + /// + /// The conversation ID. + /// True if the multiplex connection contains the specified conversation. Otherwise false. + bool Contains(long id); + + /// + /// Create a raw channel with the specified conversation ID. + /// + /// The conversation ID. + /// The remote endpoint + /// The options of the . + /// The raw channel created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions? options = null); + + /// + /// Create a conversation with the specified conversation ID. + /// + /// The conversation ID. + /// The remote endpoint + /// The options of the . + /// The KCP conversation created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, KcpConversationOptions? options = null); + + /// + /// Register a conversation or channel with the specified conversation ID and user state. + /// + /// The conversation or channel to register. + /// The conversation ID. + /// is not provided. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + void RegisterConversation(IKcpConversation conversation, long id); + + + /// + /// Unregister a conversation or channel with the specified conversation ID. + /// + /// The conversation ID. + /// The conversation unregistered. Returns null when the conversation with the specified ID is not found. + IKcpConversation? UnregisterConversation(long id); + } +} diff --git a/GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs b/GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs new file mode 100644 index 00000000..d1aa9ea8 --- /dev/null +++ b/GameServer/KcpSharp/IKcpMultiplexConnectionOfT.cs @@ -0,0 +1,54 @@ +using KcpSharp; +using System.Net; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Multiplex many channels or conversations over the same transport. + /// + public interface IKcpMultiplexConnection : IKcpMultiplexConnection + { + /// + /// Create a raw channel with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The user state of this channel. + /// The options of the . + /// The raw channel created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state, KcpRawChannelOptions? options = null); + + /// + /// Create a conversation with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The user state of this conversation. + /// The options of the . + /// The KCP conversation created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state, KcpConversationOptions? options = null); + + /// + /// Register a conversation or channel with the specified conversation ID and user state. + /// + /// The conversation or channel to register. + /// The conversation ID. + /// The user state + /// is not provided. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + void RegisterConversation(IKcpConversation conversation, long id, T? state); + + /// + /// Unregister a conversation or channel with the specified conversation ID. + /// + /// The conversation ID. + /// The user state. + /// The conversation unregistered with the user state. Returns default when the conversation with the specified ID is not found. + IKcpConversation? UnregisterConversation(long id, out T? state); + } +} diff --git a/GameServer/KcpSharp/IKcpTransport.cs b/GameServer/KcpSharp/IKcpTransport.cs new file mode 100644 index 00000000..fe14bc25 --- /dev/null +++ b/GameServer/KcpSharp/IKcpTransport.cs @@ -0,0 +1,19 @@ +using System.Net; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// A transport to send and receive packets. + /// + public interface IKcpTransport + { + /// + /// Send a packet into the transport. + /// + /// The content of the packet. + /// The remote endpoint + /// A token to cancel this operation. + /// A that completes when the packet is sent. + ValueTask SendPacketAsync(Memory packet, IPEndPoint remoteEndpoint, CancellationToken cancellationToken); + } +} diff --git a/GameServer/KcpSharp/IKcpTransportOfT.cs b/GameServer/KcpSharp/IKcpTransportOfT.cs new file mode 100644 index 00000000..6efdf80d --- /dev/null +++ b/GameServer/KcpSharp/IKcpTransportOfT.cs @@ -0,0 +1,22 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// A transport instance for upper-level connections. + /// + /// The type of the upper-level connection. + public interface IKcpTransport : IKcpTransport, IKcpExceptionProducer>, IDisposable + { + /// + /// Get the upper-level connection instace. If Start is not called or the transport is closed, will be thrown. + /// + /// Start is not called or the transport is closed. + T Connection { get; } + + /// + /// Create the upper-level connection and start pumping packets from the socket to the upper-level connection. + /// + /// The current instance is disposed. + /// has been called before. + void Start(); + } +} diff --git a/GameServer/KcpSharp/KcpAcknowledgeList.cs b/GameServer/KcpSharp/KcpAcknowledgeList.cs new file mode 100644 index 00000000..bd7093f6 --- /dev/null +++ b/GameServer/KcpSharp/KcpAcknowledgeList.cs @@ -0,0 +1,104 @@ +using System.Runtime.CompilerServices; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpAcknowledgeList + { + private readonly KcpSendQueue _sendQueue; + private (uint SerialNumber, uint Timestamp)[] _array; + private int _count; + private SpinLock _lock; + + public KcpAcknowledgeList(KcpSendQueue sendQueue, int windowSize) + { + _array = new (uint SerialNumber, uint Timestamp)[windowSize]; + _count = 0; + _lock = new SpinLock(); + _sendQueue = sendQueue; + } + + public bool TryGetAt(int index, out uint serialNumber, out uint timestamp) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + if ((uint)index >= (uint)_count) + { + serialNumber = default; + timestamp = default; + return false; + } + + (serialNumber, timestamp) = _array[index]; + return true; + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + + public void Clear() + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + _count = 0; + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + _sendQueue.NotifyAckListChanged(false); + } + + public void Add(uint serialNumber, uint timestamp) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + EnsureCapacity(); + _array[_count++] = (serialNumber, timestamp); + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + _sendQueue.NotifyAckListChanged(true); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity() + { + if (_count == _array.Length) + { + Expand(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void Expand() + { + int capacity = _count + 1; + capacity = Math.Max(capacity + capacity / 2, 16); + (uint SerialNumber, uint Timestamp)[]? newArray = new (uint SerialNumber, uint Timestamp)[capacity]; + _array.AsSpan(0, _count).CopyTo(newArray); + _array = newArray; + } + } +} diff --git a/GameServer/KcpSharp/KcpBuffer.cs b/GameServer/KcpSharp/KcpBuffer.cs new file mode 100644 index 00000000..bc002a8f --- /dev/null +++ b/GameServer/KcpSharp/KcpBuffer.cs @@ -0,0 +1,59 @@ +using System.Diagnostics; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal readonly struct KcpBuffer + { + private readonly object? _owner; + private readonly Memory _memory; + private readonly int _length; + + public ReadOnlyMemory DataRegion => _memory.Slice(0, _length); + + public int Length => _length; + + private KcpBuffer(object? owner, Memory memory, int length) + { + _owner = owner; + _memory = memory; + _length = length; + } + + public static KcpBuffer CreateFromSpan(KcpRentedBuffer buffer, ReadOnlySpan dataSource) + { + Memory memory = buffer.Memory; + if (dataSource.Length > memory.Length) + { + ThrowRentedBufferTooSmall(); + } + dataSource.CopyTo(memory.Span); + return new KcpBuffer(buffer.Owner, memory, dataSource.Length); + } + + public KcpBuffer AppendData(ReadOnlySpan data) + { + if (_length + data.Length > _memory.Length) + { + ThrowRentedBufferTooSmall(); + } + data.CopyTo(_memory.Span.Slice(_length)); + return new KcpBuffer(_owner, _memory, _length + data.Length); + } + + public KcpBuffer Consume(int length) + { + Debug.Assert((uint)length <= (uint)_length); + return new KcpBuffer(_owner, _memory.Slice(length), _length - length); + } + + public void Release() + { + new KcpRentedBuffer(_owner, _memory).Dispose(); + } + + private static void ThrowRentedBufferTooSmall() + { + throw new InvalidOperationException("The rented buffer is not large enough to hold the data."); + } + } +} diff --git a/GameServer/KcpSharp/KcpBufferPoolRentOptions.cs b/GameServer/KcpSharp/KcpBufferPoolRentOptions.cs new file mode 100644 index 00000000..9fa89dfd --- /dev/null +++ b/GameServer/KcpSharp/KcpBufferPoolRentOptions.cs @@ -0,0 +1,41 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// The options to use when renting buffers from the pool. + /// + public readonly struct KcpBufferPoolRentOptions : IEquatable + { + private readonly int _size; + private readonly bool _isOutbound; + + /// + /// The minimum size of the buffer. + /// + public int Size => _size; + + /// + /// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp. + /// + public bool IsOutbound => _isOutbound; + + /// + /// Create a with the specified parameters. + /// + /// The minimum size of the buffer. + /// True if the buffer may be passed to the outside of KcpSharp. False if the buffer is only used internally in KcpSharp. + public KcpBufferPoolRentOptions(int size, bool isOutbound) + { + _size = size; + _isOutbound = isOutbound; + } + + /// + public bool Equals(KcpBufferPoolRentOptions other) => _size == other._size && _isOutbound == other.IsOutbound; + + /// + public override bool Equals(object? obj) => obj is KcpBufferPoolRentOptions other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(_size, _isOutbound); + } +} diff --git a/GameServer/KcpSharp/KcpCommand.cs b/GameServer/KcpSharp/KcpCommand.cs new file mode 100644 index 00000000..2e8d8c5b --- /dev/null +++ b/GameServer/KcpSharp/KcpCommand.cs @@ -0,0 +1,10 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal enum KcpCommand : byte + { + Push = 81, + Ack = 82, + WindowProbe = 83, + WindowSize = 84 + } +} diff --git a/GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs b/GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs new file mode 100644 index 00000000..21dd5eb3 --- /dev/null +++ b/GameServer/KcpSharp/KcpConversation.FlushAsyncMethodBuilder.cs @@ -0,0 +1,275 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks.Sources; + +namespace KcpSharp +{ + partial class KcpConversation + { +#if NET6_0_OR_GREATER + [ThreadStatic] + private static KcpConversation? s_currentObject; + private object? _flushStateMachine; + + private struct KcpFlushAsyncMethodBuilder + { + private readonly KcpConversation _conversation; + private StateMachineBox? _task; + + private static readonly StateMachineBox s_syncSuccessSentinel = new SyncSuccessSentinelStateMachineBox(); + + public KcpFlushAsyncMethodBuilder(KcpConversation conversation) + { + _conversation = conversation; + _task = null; + } + + public static KcpFlushAsyncMethodBuilder Create() + { + KcpConversation? conversation = s_currentObject; + Debug.Assert(conversation is not null); + s_currentObject = null; + + return new KcpFlushAsyncMethodBuilder(conversation); + } + +#pragma warning disable CA1822 // Mark members as static + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Start(ref TStateMachine stateMachine) + where TStateMachine : IAsyncStateMachine +#pragma warning restore CA1822 // Mark members as static + { + Debug.Assert(stateMachine is not null); + + stateMachine.MoveNext(); + } + + public ValueTask Task + { + get + { + if (ReferenceEquals(_task, s_syncSuccessSentinel)) + { + return default; + } + StateMachineBox stateMachineBox = _task ??= CreateWeaklyTypedStateMachineBox(); + return new ValueTask(stateMachineBox, stateMachineBox.Version); + } + } + +#pragma warning disable CA1822 // Mark members as static + public void SetStateMachine(IAsyncStateMachine stateMachine) +#pragma warning restore CA1822 // Mark members as static + { + Debug.Fail("SetStateMachine should not be used."); + } + + public void SetResult() + { + if (_task == null) + { + _task = s_syncSuccessSentinel; + } + else + { + _task.SetResult(); + } + } + + public void SetException(Exception exception) + { + SetException(exception, ref _task); + } + + private static void SetException(Exception exception, ref StateMachineBox? boxFieldRef) + { + if (exception == null) + { + throw new ArgumentNullException(nameof(exception)); + } + (boxFieldRef ??= CreateWeaklyTypedStateMachineBox()).SetException(exception); + } + + public void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : INotifyCompletion + where TStateMachine : IAsyncStateMachine + { + AwaitOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine) + where TAwaiter : ICriticalNotifyCompletion + where TStateMachine : IAsyncStateMachine + { + AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine, ref _task, _conversation); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? boxRef, KcpConversation conversation) where TAwaiter : ICriticalNotifyCompletion where TStateMachine : IAsyncStateMachine + { + StateMachineBox stateMachineBox = GetStateMachineBox(ref stateMachine, ref boxRef, conversation); + AwaitUnsafeOnCompleted(ref awaiter, stateMachineBox); + } + + private static void AwaitOnCompleted(ref TAwaiter awaiter, ref TStateMachine stateMachine, ref StateMachineBox? box, KcpConversation conversation) where TAwaiter : INotifyCompletion where TStateMachine : IAsyncStateMachine + { + try + { + awaiter.OnCompleted(GetStateMachineBox(ref stateMachine, ref box, conversation).MoveNextAction); + } + catch (Exception exception) + { + ExceptionDispatchInfo? edi = ExceptionDispatchInfo.Capture(exception); + ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi); + } + } + + private static void AwaitUnsafeOnCompleted(ref TAwaiter awaiter, StateMachineBox box) where TAwaiter : ICriticalNotifyCompletion + { + try + { + awaiter.UnsafeOnCompleted(box.MoveNextAction); + } + catch (Exception exception) + { + ExceptionDispatchInfo? edi = ExceptionDispatchInfo.Capture(exception); + ThreadPool.QueueUserWorkItem(static state => ((ExceptionDispatchInfo)state!).Throw(), edi); + } + } + + + private static StateMachineBox CreateWeaklyTypedStateMachineBox() + { + return new StateMachineBox(null); + } + + private static StateMachineBox GetStateMachineBox(ref TStateMachine stateMachine, ref StateMachineBox? boxFieldRef, KcpConversation conversation) where TStateMachine : IAsyncStateMachine + { + StateMachineBox? stateMachineBox = boxFieldRef as StateMachineBox; + if (stateMachineBox != null) + { + return stateMachineBox; + } + StateMachineBox? stateMachineBox2 = boxFieldRef as StateMachineBox; + if (stateMachineBox2 != null) + { + if (stateMachineBox2.StateMachine == null) + { + Debugger.NotifyOfCrossThreadDependency(); + stateMachineBox2.StateMachine = stateMachine; + } + return stateMachineBox2; + } + Debugger.NotifyOfCrossThreadDependency(); + StateMachineBox stateMachineBox3 = (StateMachineBox)(boxFieldRef = StateMachineBox.GetOrCreateBox(conversation)); + stateMachineBox3.StateMachine = stateMachine; + return stateMachineBox3; + } + + private abstract class StateMachineBox : IValueTaskSource + { + protected ManualResetValueTaskSourceCore _mrvtsc; + protected Action? _moveNextAction; + + public virtual Action MoveNextAction => _moveNextAction!; + + public short Version => _mrvtsc.Version; + + public void SetResult() + { + _mrvtsc.SetResult(true); + } + + public void SetException(Exception error) + { + _mrvtsc.SetException(error); + } + + public ValueTaskSourceStatus GetStatus(short token) + { + return _mrvtsc.GetStatus(token); + } + + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + { + _mrvtsc.OnCompleted(continuation, state, token, flags); + } + + void IValueTaskSource.GetResult(short token) + { + throw new NotSupportedException(); + } + } + + private sealed class SyncSuccessSentinelStateMachineBox : StateMachineBox + { + public SyncSuccessSentinelStateMachineBox() + { + SetResult(); + } + } + + + private sealed class StateMachineBox : StateMachineBox, IValueTaskSource where TStateMachine : IAsyncStateMachine + { + [MaybeNull] + public TStateMachine StateMachine; + private KcpConversation? _conversation; + + public override Action MoveNextAction => _moveNextAction ??= MoveNext; + + internal StateMachineBox(KcpConversation? conversation) + { + _conversation = conversation; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static StateMachineBox GetOrCreateBox(KcpConversation conversation) + { + if (conversation._flushStateMachine is StateMachineBox stateMachine) + { + stateMachine._conversation = conversation; + conversation._flushStateMachine = null; + return stateMachine; + } + return new StateMachineBox(conversation); + } + + void IValueTaskSource.GetResult(short token) + { + try + { + _mrvtsc.GetResult(token); + } + finally + { + ReturnOrDropBox(); + } + } + + public void MoveNext() + { + if (StateMachine is not null) + { + StateMachine.MoveNext(); + } + } + + private void ReturnOrDropBox() + { + StateMachine = default!; + _mrvtsc.Reset(); + if (_conversation is not null) + { + _conversation._flushStateMachine = this; + _conversation = null; + } + } + } + } +#endif + } +} diff --git a/GameServer/KcpSharp/KcpConversation.cs b/GameServer/KcpSharp/KcpConversation.cs new file mode 100644 index 00000000..a6fe7ef0 --- /dev/null +++ b/GameServer/KcpSharp/KcpConversation.cs @@ -0,0 +1,1399 @@ +using System.Buffers.Binary; +using System.Runtime.CompilerServices; +using System.Net; +using System.Net.Sockets; +using EggLink.DanhengServer.Util; +using EggLink.DanhengServer.KcpSharp; + + + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList; +using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode; +#else +using LinkedListOfBufferItem = System.Collections.Generic.LinkedList; +using LinkedListNodeOfBufferItem = System.Collections.Generic.LinkedListNode; +#endif + +namespace KcpSharp +{ + /// + /// A reliable channel over an unreliable transport implemented in KCP protocol. + /// + public sealed partial class KcpConversation : IKcpConversation, IKcpExceptionProducer + { + private readonly IKcpBufferPool _bufferPool; + private readonly IKcpTransport _transport; + private readonly IPEndPoint _remoteEndPoint; + private readonly ulong? _id; + + private readonly int _mtu; + private readonly int _mss; + private readonly int _preBufferSize; + private readonly int _postBufferSize; + + private uint _snd_una; + private uint _snd_nxt; + private uint _rcv_nxt; + + private uint _ssthresh; + + private int _rx_rttval; + private int _rx_srtt; + private uint _rx_rto; + private uint _rx_minrto; + + private uint _snd_wnd; + private uint _rcv_wnd; + private uint _rmt_wnd; + private uint _cwnd; + private KcpProbeType _probe; + private SpinLock _cwndUpdateLock; + + private uint _interval; + private uint _ts_flush; + + private bool _nodelay; + private uint _ts_probe; + private uint _probe_wait; + + private uint _incr; + + private readonly KcpSendReceiveQueueItemCache _queueItemCache; + private readonly KcpSendQueue _sendQueue; + private readonly KcpReceiveQueue _receiveQueue; + + private readonly LinkedListOfBufferItem _sndBuf = new(); + private readonly LinkedListOfBufferItem _rcvBuf = new(); + private KcpSendReceiveBufferItemCache _cache = KcpSendReceiveBufferItemCache.Create(); + + private readonly KcpAcknowledgeList _ackList; + + private int _fastresend; + private int _fastlimit; + private bool _nocwnd; + private bool _stream; + + private bool _keepAliveEnabled; + private uint _keepAliveInterval; + private uint _keepAliveGracePeriod; + private uint _lastReceiveTick; + private uint _lastSendTick; + + private KcpReceiveWindowNotificationOptions? _receiveWindowNotificationOptions; + private uint _ts_rcv_notify; + private uint _ts_rcv_notify_wait; + + private KcpConversationUpdateActivation? _updateActivation; + private CancellationTokenSource? _updateLoopCts; + private bool _transportClosed; + private bool _disposed; + + private Func? _exceptionHandler; + private object? _exceptionHandlerState; + + private const uint IKCP_RTO_MAX = 60000; + private const int IKCP_THRESH_MIN = 2; + private const uint IKCP_PROBE_INIT = 7000; // 7 secs to probe window size + private const uint IKCP_PROBE_LIMIT = 120000; // up to 120 secs to probe window + + /// + /// Construct a reliable channel using KCP protocol. + /// + /// The remote endpoint + /// The underlying transport. + /// The options of the . + public KcpConversation(IPEndPoint remoteEndpoint, IKcpTransport transport, KcpConversationOptions? options) + : this(remoteEndpoint, transport, null, options) + { } + + /// + /// Construct a reliable channel using KCP protocol. + /// + /// The remote endpoint + /// The underlying transport. + /// The conversation ID. + /// The options of the . + public KcpConversation(IPEndPoint remoteEndpoint, IKcpTransport transport, long conversationId, KcpConversationOptions? options) + : this(remoteEndpoint, transport, (ulong)conversationId, options) + { } + + private KcpConversation(IPEndPoint remoteEndpoint, IKcpTransport transport, ulong? conversationId, KcpConversationOptions? options) + { + _bufferPool = options?.BufferPool ?? DefaultArrayPoolBufferAllocator.Default; + _transport = transport; + _remoteEndPoint = remoteEndpoint; + _id = conversationId; + + if (options is null) + { + _mtu = KcpConversationOptions.MtuDefaultValue; + } + else if (options.Mtu < 50) + { + throw new ArgumentException("MTU must be at least 50.", nameof(options)); + } + else + { + _mtu = options.Mtu; + } + + _preBufferSize = options?.PreBufferSize ?? 0; + _postBufferSize = options?.PostBufferSize ?? 0; + if (_preBufferSize < 0) + { + throw new ArgumentException("PreBufferSize must be a non-negative integer.", nameof(options)); + } + if (_postBufferSize < 0) + { + throw new ArgumentException("PostBufferSize must be a non-negative integer.", nameof(options)); + } + if ((uint)(_preBufferSize + _postBufferSize) >= (uint)(_mtu - KcpGlobalVars.HEADER_LENGTH_WITHOUT_CONVID)) + { + throw new ArgumentException("The sum of PreBufferSize and PostBufferSize is too large. There is not enough space in the packet for the KCP header.", nameof(options)); + } + if (conversationId.HasValue && (uint)(_preBufferSize + _postBufferSize) >= (uint)(_mtu - KcpGlobalVars.HEADER_LENGTH_WITH_CONVID)) + { + throw new ArgumentException("The sum of PreBufferSize and PostBufferSize is too large. There is not enough space in the packet for the KCP header.", nameof(options)); + } + + _mss = conversationId.HasValue ? _mtu - KcpGlobalVars.HEADER_LENGTH_WITH_CONVID : _mtu - KcpGlobalVars.HEADER_LENGTH_WITHOUT_CONVID; + _mss = _mss - _preBufferSize - _postBufferSize; + + _ssthresh = 2; + + _nodelay = options is not null && options.NoDelay; + if (_nodelay) + { + _rx_minrto = 30; + } + else + { + _rx_rto = 200; + _rx_minrto = 100; + } + + _snd_wnd = options is null || options.SendWindow <= 0 ? KcpConversationOptions.SendWindowDefaultValue : (uint)options.SendWindow; + _rcv_wnd = options is null || options.ReceiveWindow <= 0 ? KcpConversationOptions.ReceiveWindowDefaultValue : (uint)options.ReceiveWindow; + _rmt_wnd = options is null || options.RemoteReceiveWindow <= 0 ? KcpConversationOptions.RemoteReceiveWindowDefaultValue : (uint)options.RemoteReceiveWindow; + _rcv_nxt = 0; + + _interval = options is null || options.UpdateInterval < 10 ? KcpConversationOptions.UpdateIntervalDefaultValue : (uint)options.UpdateInterval; + + _fastresend = options is null ? 0 : options.FastResend; + _fastlimit = 5; + _nocwnd = options is not null && options.DisableCongestionControl; + _stream = options is not null && options.StreamMode; + + _updateActivation = new KcpConversationUpdateActivation((int)_interval); + _queueItemCache = new KcpSendReceiveQueueItemCache(); + _sendQueue = new KcpSendQueue(_bufferPool, _updateActivation, _stream, options is null || options.SendQueueSize <= 0 ? KcpConversationOptions.SendQueueSizeDefaultValue : options.SendQueueSize, _mss, _queueItemCache); + _receiveQueue = new KcpReceiveQueue(_stream, options is null || options.ReceiveQueueSize <= 0 ? KcpConversationOptions.ReceiveQueueSizeDefaultValue : options.ReceiveQueueSize, _queueItemCache); + _ackList = new KcpAcknowledgeList(_sendQueue, (int)_snd_wnd); + + _updateLoopCts = new CancellationTokenSource(); + + _ts_flush = GetTimestamp(); + + _lastSendTick = _ts_flush; + _lastReceiveTick = _ts_flush; + KcpKeepAliveOptions? keepAliveOptions = options?.KeepAliveOptions; + if (keepAliveOptions is not null) + { + _keepAliveEnabled = true; + _keepAliveInterval = (uint)keepAliveOptions.SendInterval; + _keepAliveGracePeriod = (uint)keepAliveOptions.GracePeriod; + } + + _receiveWindowNotificationOptions = options?.ReceiveWindowNotificationOptions; + if (_receiveWindowNotificationOptions is not null) + { + _ts_rcv_notify_wait = 0; + _ts_rcv_notify = _ts_flush + (uint)_receiveWindowNotificationOptions.InitialInterval; + } + + RunUpdateOnActivation(); + } + + /// + /// Set the handler to invoke when exception is thrown during flushing packets to the transport. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation and mark the transport as closed. + /// + /// The exception handler. + /// The state object to pass into the exception handler. + public void SetExceptionHandler(Func handler, object? state) + { + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _exceptionHandler = handler; + _exceptionHandlerState = state; + } + + /// + /// Get the ID of the current conversation. + /// + public long? ConversationId => (long?)_id; + + /// + /// Get whether the transport is marked as closed. + /// + public bool TransportClosed => _transportClosed; + + /// + /// Get whether the conversation is in stream mode. + /// + public bool StreamMode => _stream; + + /// + /// Get the available byte count and available segment count in the send queue. + /// + /// The available byte count in the send queue. + /// The available segment count in the send queue. + /// True if the transport is not closed. Otherwise false. + public bool TryGetSendQueueAvailableSpace(out int byteCount, out int segmentCount) + => _sendQueue.TryGetAvailableSpace(out byteCount, out segmentCount); + + /// + /// Try to put message into the send queue. + /// + /// The content of the message. + /// True if the message is put into the send queue. False if the message is too large to fit in the send queue, or the transport is closed. + /// The size of the message is larger than 256 * mtu, thus it can not be correctly fragmented and sent. This exception is never thrown in stream mode. + /// The send or flush operation is initiated concurrently. + public bool TrySend(ReadOnlySpan buffer) + => _sendQueue.TrySend(buffer, false, out _); + + /// + /// Try to put message into the send queue. + /// + /// The content of the message. + /// Whether partial sending is allowed in stream mode. This must not be true in non-stream mode. + /// The number of bytes put into the send queue. This is always the same as the size of the unless is set to true. + /// True if the message is put into the send queue. False if the message is too large to fit in the send queue, or the transport is closed. + /// is set to true in non-stream mode. Or the size of the message is larger than 256 * mtu, thus it can not be correctly fragmented and sent. This exception is never thrown in stream mode. + /// The send or flush operation is initiated concurrently. + public bool TrySend(ReadOnlySpan buffer, bool allowPartialSend, out int bytesWritten) + => _sendQueue.TrySend(buffer, allowPartialSend, out bytesWritten); + + /// + /// Wait until the send queue contains at least bytes of free space, and also available segments. + /// + /// The number of bytes in the available space. + /// The count of segments in the available space. + /// The token to cancel this operation. + /// or is larger than the total space of the send queue. + /// The is fired before send operation is completed. Or is called before this operation is completed. + /// A that completes when there is enough space in the send queue. The result of the task is false when the transport is closed. + public ValueTask WaitForSendQueueAvailableSpaceAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken = default) + => _sendQueue.WaitForAvailableSpaceAsync(minimumBytes, minimumSegments, cancellationToken); + + /// + /// Put message into the send queue. + /// + /// The content of the message. + /// The token to cancel this operation. + /// The size of the message is larger than 256 * mtu, thus it can not be correctly fragmented and sent. This exception is never thrown in stream mode. + /// The is fired before send operation is completed. Or is called before this operation is completed. + /// The send or flush operation is initiated concurrently. + /// A that completes when the entire message is put into the queue. The result of the task is false when the transport is closed. + public ValueTask SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => _sendQueue.SendAsync(buffer, cancellationToken); + + internal ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + => _sendQueue.WriteAsync(buffer, cancellationToken); + + /// + /// Cancel the current send operation or flush operation. + /// + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingSend() + => _sendQueue.CancelPendingOperation(null, default); + + /// + /// Cancel the current send operation or flush operation. + /// + /// The inner exception of the thrown by the method or method. + /// The in the thrown by the method or method. + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingSend(Exception? innerException, CancellationToken cancellationToken) + => _sendQueue.CancelPendingOperation(innerException, cancellationToken); + + /// + /// Gets the count of bytes not yet sent to the remote host or not acknowledged by the remote host. + /// + public long UnflushedBytes => _sendQueue.GetUnflushedBytes(); + + /// + /// Wait until all messages are sent and acknowledged by the remote host, as well as all the acknowledgements are sent. + /// + /// The token to cancel this operation. + /// The is fired before send operation is completed. Or is called before this operation is completed. + /// The send or flush operation is initiated concurrently. + /// The instance is disposed. + /// A that completes when the all messages are sent and acknowledged. The result of the task is false when the transport is closed. + public ValueTask FlushAsync(CancellationToken cancellationToken = default) + => _sendQueue.FlushAsync(cancellationToken); + + internal ValueTask FlushForStreamAsync(CancellationToken cancellationToken) + => _sendQueue.FlushForStreamAsync(cancellationToken); + +#if !NET6_0_OR_GREATER + private ValueTask FlushCoreAsync(CancellationToken cancellationToken) + => new ValueTask(FlushCore2Async(cancellationToken)); + + private async Task FlushCore2Async(CancellationToken cancellationToken) +#else + private ValueTask FlushCoreAsync(CancellationToken cancellationToken) + { + s_currentObject = this; + return FlushCore2Async(cancellationToken); + } + + [AsyncMethodBuilder(typeof(KcpFlushAsyncMethodBuilder))] + private async ValueTask FlushCore2Async(CancellationToken cancellationToken) +#endif + { + int preBufferSize = _preBufferSize; + int postBufferSize = _postBufferSize; + int packetHeaderSize = _id.HasValue ? KcpGlobalVars.HEADER_LENGTH_WITH_CONVID : KcpGlobalVars.HEADER_LENGTH_WITHOUT_CONVID; + int sizeLimitBeforePostBuffer = _mtu - _postBufferSize; + bool anyPacketSent = false; + + ushort windowSize = (ushort)GetUnusedReceiveWindow(); + uint unacknowledged = _rcv_nxt; + + using KcpRentedBuffer bufferOwner = _bufferPool.Rent(new KcpBufferPoolRentOptions(_mtu + (_mtu - preBufferSize - postBufferSize), true)); + Memory buffer = bufferOwner.Memory; + int size = preBufferSize; + buffer.Span.Slice(0, size).Clear(); + + // flush acknowledges + { + int index = 0; + while (_ackList.TryGetAt(index++, out uint serialNumber, out uint timestamp)) + { + if ((size + packetHeaderSize) > sizeLimitBeforePostBuffer) + { + buffer.Span.Slice(size, postBufferSize).Clear(); + await _transport.SendPacketAsync(buffer.Slice(0, size + postBufferSize), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + _lastSendTick = GetTimestamp(); + size = preBufferSize; + buffer.Span.Slice(0, size).Clear(); + anyPacketSent = true; + } + KcpPacketHeader header = new(KcpCommand.Ack, 0, windowSize, timestamp, serialNumber, unacknowledged); + header.EncodeHeader(_id, 0, buffer.Span.Slice(size), out int bytesWritten); + size += bytesWritten; + } + } + + uint current = GetTimestamp(); + + // calculate window size + uint cwnd = Math.Min(_snd_wnd, _rmt_wnd); + if (!_nocwnd) + { + cwnd = Math.Min(_cwnd, cwnd); + } + + // move data from snd_queue to snd_buf + while (TimeDiff(_snd_nxt, _snd_una + cwnd) < 0) + { + if (!_sendQueue.TryDequeue(out KcpBuffer data, out byte fragment)) + { + break; + } + + lock (_sndBuf) + { + if (_transportClosed) + { + data.Release(); + return; + } + + _sndBuf.AddLast(CreateSendBufferItem(in data, fragment, current, windowSize, (uint)Interlocked.Increment(ref Unsafe.As(ref _snd_nxt)) - 1, unacknowledged, _rx_rto)); + } + } + + // calculate resent + uint resent = _fastresend > 0 ? (uint)_fastresend : 0xffffffff; + uint rtomin = !_nodelay ? (_rx_rto >> 3) : 0; + + // flush data segments + bool lost = false; + bool change = false; + LinkedListNodeOfBufferItem? segmentNode = _sndBuf.First; + while (segmentNode is not null && !_transportClosed) + { + LinkedListNodeOfBufferItem? nextSegmentNode = segmentNode.Next; + + bool needsend = false; + KcpSendSegmentStats stats = segmentNode.ValueRef.Stats; + + if (segmentNode.ValueRef.Stats.TransmitCount == 0) + { + needsend = true; + segmentNode.ValueRef.Stats = new KcpSendSegmentStats(current + segmentNode.ValueRef.Stats.Rto + rtomin, _rx_rto, stats.FastAck, stats.TransmitCount + 1); + } + else if (TimeDiff(current, stats.ResendTimestamp) >= 0) + { + needsend = true; + uint rto = stats.Rto; + if (!_nodelay) + { + rto += Math.Max(stats.Rto, _rx_rto); + } + else + { + uint step = rto; //_nodelay < 2 ? segment.rto : _rx_rto; + rto += step / 2; + } + segmentNode.ValueRef.Stats = new KcpSendSegmentStats(current + rto, rto, stats.FastAck, stats.TransmitCount + 1); + lost = true; + } + else if (stats.FastAck > resent) + { + if (stats.TransmitCount <= _fastlimit || _fastlimit == 0) + { + needsend = true; + segmentNode.ValueRef.Stats = new KcpSendSegmentStats(current, stats.Rto, 0, stats.TransmitCount + 1); + change = true; + } + } + + if (needsend) + { + KcpPacketHeader header = DeplicateHeader(ref segmentNode.ValueRef.Segment, current, windowSize, unacknowledged); + + int need = packetHeaderSize + segmentNode.ValueRef.Data.Length; + if ((size + need) > sizeLimitBeforePostBuffer) + { + buffer.Span.Slice(size, postBufferSize).Clear(); + await _transport.SendPacketAsync(buffer.Slice(0, size + postBufferSize), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + _lastSendTick = GetTimestamp(); + size = preBufferSize; + buffer.Span.Slice(0, size).Clear(); + anyPacketSent = true; + } + + lock (segmentNode) + { + KcpBuffer data = segmentNode.ValueRef.Data; + if (!_transportClosed) + { + header.EncodeHeader(_id, data.Length, buffer.Span.Slice(size), out int bytesWritten); + + size += bytesWritten; + + if (data.Length > 0) + { + data.DataRegion.CopyTo(buffer.Slice(size)); + size += data.Length; + } + } + } + } + + segmentNode = nextSegmentNode; + } + + _ackList.Clear(); + + // probe window size (if remote window size equals zero) + if (_rmt_wnd == 0) + { + if (_probe_wait == 0) + { + _probe_wait = IKCP_PROBE_INIT; + _ts_probe = current + _probe_wait; + } + else + { + if (TimeDiff(current, _ts_probe) >= 0) + { + if (_probe_wait < IKCP_PROBE_INIT) + { + _probe_wait = IKCP_PROBE_INIT; + } + _probe_wait += _probe_wait / 2; + if (_probe_wait > IKCP_PROBE_LIMIT) + { + _probe_wait = IKCP_PROBE_LIMIT; + } + _ts_probe = current + _probe_wait; + _probe |= KcpProbeType.AskSend; + } + } + } + else + { + _ts_probe = 0; + _probe_wait = 0; + } + + // flush window probing command + if ((_probe & KcpProbeType.AskSend) != 0) + { + if ((size + packetHeaderSize) > sizeLimitBeforePostBuffer) + { + buffer.Span.Slice(size, postBufferSize).Clear(); + await _transport.SendPacketAsync(buffer.Slice(0, size + postBufferSize), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + _lastSendTick = GetTimestamp(); + size = preBufferSize; + buffer.Span.Slice(0, size).Clear(); + anyPacketSent = true; + } + KcpPacketHeader header = new(KcpCommand.WindowProbe, 0, windowSize, 0, 0, unacknowledged); + header.EncodeHeader(_id, 0, buffer.Span.Slice(size), out int bytesWritten); + size += bytesWritten; + } + + // flush window probing response + if (!anyPacketSent && ShouldSendWindowSize(current)) + { + if ((size + packetHeaderSize) > sizeLimitBeforePostBuffer) + { + buffer.Span.Slice(size, postBufferSize).Clear(); + await _transport.SendPacketAsync(buffer.Slice(0, size + postBufferSize), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + _lastSendTick = GetTimestamp(); + size = preBufferSize; + buffer.Span.Slice(0, size).Clear(); + } + KcpPacketHeader header = new(KcpCommand.WindowSize, 0, windowSize, 0, 0, unacknowledged); + header.EncodeHeader(_id, 0, buffer.Span.Slice(size), out int bytesWritten); + size += bytesWritten; + } + + _probe = KcpProbeType.None; + + // flush remaining segments + if (size > preBufferSize) + { + buffer.Span.Slice(size, postBufferSize).Clear(); + try + { + await _transport.SendPacketAsync(buffer.Slice(0, size + postBufferSize), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + new Logger("KcpServer").Error("transport send error", ex); + } + _lastSendTick = GetTimestamp(); + } + + // update window + bool lockTaken = false; + try + { + _cwndUpdateLock.Enter(ref lockTaken); + + uint updatedCwnd = _cwnd; + uint incr = _incr; + + // update sshthresh + if (change) + { + uint inflight = _snd_nxt - _snd_una; + _ssthresh = Math.Max(inflight / 2, IKCP_THRESH_MIN); + updatedCwnd = _ssthresh + resent; + incr = updatedCwnd * (uint)_mss; + } + + if (lost) + { + _ssthresh = Math.Max(cwnd / 2, IKCP_THRESH_MIN); + updatedCwnd = 1; + incr = (uint)_mss; + } + + if (updatedCwnd < 1) + { + updatedCwnd = 1; + incr = (uint)_mss; + } + + _cwnd = updatedCwnd; + _incr = incr; + } + finally + { + if (lockTaken) + { + _cwndUpdateLock.Exit(); + } + } + + // send keep-alive + if (_keepAliveEnabled) + { + if (TimeDiff(GetTimestamp(), _lastSendTick) > _keepAliveInterval) + { + KcpPacketHeader header = new(KcpCommand.WindowSize, 0, windowSize, 0, 0, unacknowledged); + header.EncodeHeader(_id, 0, buffer.Span, out int bytesWritten); + await _transport.SendPacketAsync(buffer.Slice(0, bytesWritten), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + _lastSendTick = GetTimestamp(); + } + } + } + + private bool ShouldSendWindowSize(uint current) + { + if ((_probe & KcpProbeType.AskTell) != 0) + { + return true; + } + + KcpReceiveWindowNotificationOptions? options = _receiveWindowNotificationOptions; + if (options is null) + { + return false; + } + + if (TimeDiff(current, _ts_rcv_notify) < 0) + { + return false; + } + + uint inital = (uint)options.InitialInterval; + uint maximum = (uint)options.MaximumInterval; + if (_ts_rcv_notify_wait < inital) + { + _ts_rcv_notify_wait = inital; + } + else if (_ts_rcv_notify_wait >= maximum) + { + _ts_rcv_notify_wait = maximum; + } + else + { + _ts_rcv_notify_wait = Math.Min(maximum, _ts_rcv_notify_wait + _ts_rcv_notify_wait / 2); + } + _ts_rcv_notify = current + _ts_rcv_notify_wait; + + return true; + } + + private LinkedListNodeOfBufferItem CreateSendBufferItem(in KcpBuffer data, byte fragment, uint current, ushort windowSize, uint serialNumber, uint unacknowledged, uint rto) + { + KcpSendReceiveBufferItem newseg = new() + { + Data = data, + Segment = new KcpPacketHeader(KcpCommand.Push, fragment, windowSize, current, serialNumber, unacknowledged), + Stats = new KcpSendSegmentStats(current, rto, 0, 0) + }; + return _cache.Allocate(in newseg); + } + + private static KcpPacketHeader DeplicateHeader(ref KcpPacketHeader header, uint timestamp, ushort windowSize, uint unacknowledged) + { + return new KcpPacketHeader(header.Command, header.Fragment, windowSize, timestamp, header.SerialNumber, unacknowledged); + } + + private uint GetUnusedReceiveWindow() + { + uint count = (uint)_receiveQueue.GetQueueSize(); + if (count < _rcv_wnd) + { + return _rcv_wnd - count; + } + return 0; + } + + private async void RunUpdateOnActivation() + { + CancellationToken cancellationToken = _updateLoopCts?.Token ?? new CancellationToken(true); + KcpConversationUpdateActivation? activation = _updateActivation; + if (activation is null) + { + return; + } + + while (!cancellationToken.IsCancellationRequested) + { + bool update = false; + using (KcpConversationUpdateNotification notification = await activation.WaitAsync(CancellationToken.None).ConfigureAwait(false)) + { + if (_transportClosed) + { + break; + } + + ReadOnlyMemory packet = notification.Packet; + if (!packet.IsEmpty) + { + try + { + update = SetInput(packet.Span); + } + catch (Exception ex) + { + new Logger("KcpServer").Error("Update error", ex); + } + } + + if (_transportClosed) + { + break; + } + + update |= notification.TimerNotification; + } + + try + { + if (update) + { + await UpdateCoreAsync(cancellationToken).ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + if (!HandleFlushException(ex)) + { + break; + } + } + + if (_keepAliveEnabled && TimeDiff(GetTimestamp(), _lastReceiveTick) > _keepAliveGracePeriod) + { + SetTransportClosed(); + } + } + } + + private ValueTask UpdateCoreAsync(CancellationToken cancellationToken) + { + uint current = GetTimestamp(); + long slap = TimeDiff(current, _ts_flush); + if (slap > 10000 || slap < -10000) + { + _ts_flush = current; + slap = 0; + } + + if (slap >= 0 || _nodelay) + { + _ts_flush += _interval; + if (TimeDiff(current, _ts_flush) >= 0) + { + _ts_flush = current + _interval; + } + return FlushCoreAsync(cancellationToken); + } + return default; + } + + private bool HandleFlushException(Exception ex) + { + Func? handler = _exceptionHandler; + object? state = _exceptionHandlerState; + bool result = false; + if (handler is not null) + { + try + { + result = handler.Invoke(ex, this, state); + } + catch + { + result = false; + } + } + + if (!result) + { + SetTransportClosed(); + } + return result; + } + + /// + public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default) + { + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + int packetHeaderSize = _id.HasValue ? KcpGlobalVars.HEADER_LENGTH_WITH_CONVID : KcpGlobalVars.HEADER_LENGTH_WITHOUT_CONVID; + if (packet.Buffer.Length < packetHeaderSize) + { + return default; + } + + ReadOnlySpan packetSpan = packet.Buffer.AsSpan(); + if (_id.HasValue) + { + ulong conversationId = BinaryPrimitives.ReadUInt64BigEndian(packet.Buffer.AsSpan()); + if (conversationId != _id.GetValueOrDefault()) + { + return default; + } + packetSpan = packetSpan.Slice(8); + } + + uint length = BinaryPrimitives.ReadUInt32LittleEndian(packetSpan.Slice(16)); + if (length > (uint)(packetSpan.Length - 20)) // implicitly checked for (int)length < 0 + { + return default; + } + + KcpConversationUpdateActivation? activation = _updateActivation; + if (activation is null) + { + return default; + } + + return activation.InputPacketAsync(packet.Buffer.AsMemory(), cancellationToken); + } + + private bool SetInput(ReadOnlySpan packet) + { + uint current = GetTimestamp(); + int packetHeaderSize = _id.HasValue ? 28 : 20; + + uint prev_una = _snd_una; + uint maxack = 0, latest_ts = 0; + bool flag = false; + bool mutated = false; + + while (true) + { + if (packet.Length < packetHeaderSize) + { + break; + } + + if (_id.HasValue) + { + if (BinaryPrimitives.ReadUInt64BigEndian(packet) != _id.GetValueOrDefault()) + { + return mutated; + } + packet = packet.Slice(8); + } + + KcpPacketHeader header = KcpPacketHeader.Parse(packet); + int length = BinaryPrimitives.ReadInt32LittleEndian(packet.Slice(16)); + + packet = packet.Slice(20); + if ((uint)length > (uint)packet.Length) + { + return mutated; + } + + if (header.Command != KcpCommand.Push && + header.Command != KcpCommand.Ack && + header.Command != KcpCommand.WindowProbe && + header.Command != KcpCommand.WindowSize) + { + return mutated; + } + + _lastReceiveTick = current; + _rmt_wnd = header.WindowSize; + mutated = HandleUnacknowledged(header.Unacknowledged) | mutated; + mutated = UpdateSendUnacknowledged() | mutated; + + if (header.Command == KcpCommand.Ack) + { + int rtt = TimeDiff(current, header.Timestamp); + if (rtt >= 0) + { + UpdateRto(rtt); + } + mutated = HandleAck(header.SerialNumber) | mutated; + mutated = UpdateSendUnacknowledged() | mutated; + + if (!flag) + { + flag = true; + maxack = header.SerialNumber; + latest_ts = header.Timestamp; + } + else + { + if (TimeDiff(_snd_nxt, maxack) > 0) + { +#if !IKCP_FASTACK_CONSERVE + maxack = header.SerialNumber; + latest_ts = header.Timestamp; +#else + if (TimeDiff(header.Timestamp, latest_ts) > 0) { + maxack = header.SerialNumber; + latest_ts = header.Timestamp; + } +#endif + } + } + } + else if (header.Command == KcpCommand.Push) + { + if (TimeDiff(header.SerialNumber, _rcv_nxt + _rcv_wnd) < 0) + { + AckPush(header.SerialNumber, header.Timestamp); + if (TimeDiff(header.SerialNumber, _rcv_nxt) >= 0) + { + mutated = HandleData(header, packet.Slice(0, length)) | mutated; + } + + if (_receiveWindowNotificationOptions is not null) + { + if (_ts_rcv_notify_wait != 0) + { + _ts_rcv_notify_wait = 0; + _ts_rcv_notify = current + (uint)_receiveWindowNotificationOptions.InitialInterval; + } + } + } + } + else if (header.Command == KcpCommand.WindowProbe) + { + _probe |= KcpProbeType.AskTell; + } + else if (header.Command == KcpCommand.WindowSize) + { + // do nothing + } + else + { + return mutated; + } + + packet = packet.Slice(length); + } + + if (flag) + { + HandleFastAck(maxack, latest_ts); + } + + if (TimeDiff(_snd_una, prev_una) > 0) + { + bool lockTaken = false; + try + { + _cwndUpdateLock.Enter(ref lockTaken); + + uint cwnd = _cwnd; + uint incr = _incr; + + if (cwnd < _rmt_wnd) + { + uint mss = (uint)_mss; + if (cwnd < _ssthresh) + { + cwnd++; + incr += mss; + } + else + { + if (incr < mss) + { + incr = mss; + } + incr += (mss * mss) / incr + mss / 16; + cwnd = (incr + mss - 1) / (mss > 0 ? mss : 1); + } + if (cwnd > _rmt_wnd) + { + cwnd = _rmt_wnd; + incr = _rmt_wnd * mss; + } + } + + _cwnd = cwnd; + _incr = incr; + } + finally + { + if (lockTaken) + { + _cwndUpdateLock.Exit(); + } + } + } + + return mutated; + } + + private bool HandleUnacknowledged(uint una) + { + bool mutated = false; + lock (_sndBuf) + { + LinkedListNodeOfBufferItem? node = _sndBuf.First; + while (node is not null) + { + LinkedListNodeOfBufferItem? next = node.Next; + + if (TimeDiff(una, node.ValueRef.Segment.SerialNumber) > 0) + { + _sndBuf.Remove(node); + ref KcpBuffer dataRef = ref node.ValueRef.Data; + _sendQueue.SubtractUnflushedBytes(dataRef.Length); + dataRef.Release(); + dataRef = default; + _cache.Return(node); + mutated = true; + } + else + { + break; + } + + node = next; + } + } + return mutated; + } + + private bool UpdateSendUnacknowledged() + { + lock (_sndBuf) + { + LinkedListNodeOfBufferItem? first = _sndBuf.First; + uint snd_una = first is null ? _snd_nxt : first.ValueRef.Segment.SerialNumber; + uint old_snd_una = (uint)Interlocked.Exchange(ref Unsafe.As(ref _snd_una), (int)snd_una); + return snd_una != old_snd_una; + } + } + + private void UpdateRto(int rtt) + { + if (_rx_srtt == 0) + { + _rx_srtt = rtt; + _rx_rttval = rtt / 2; + } + else + { + int delta = rtt - _rx_srtt; + if (delta < 0) + { + delta = -delta; + } + _rx_rttval = (3 * _rx_rttval + delta) / 4; + _rx_srtt = (7 * _rx_srtt + rtt) / 8; + if (_rx_srtt < 1) + { + _rx_srtt = 1; + } + } + int rto = _rx_srtt + Math.Max((int)_interval, 4 * _rx_rttval); +#if NEED_MATH_SHIM + _rx_rto = Math.Min(Math.Max((uint)rto, _rx_minrto), IKCP_RTO_MAX); +#else + _rx_rto = Math.Clamp((uint)rto, _rx_minrto, IKCP_RTO_MAX); +#endif + } + + private bool HandleAck(uint serialNumber) + { + if (TimeDiff(serialNumber, _snd_una) < 0 || TimeDiff(serialNumber, _snd_nxt) >= 0) + { + return false; + } + + lock (_sndBuf) + { + LinkedListNodeOfBufferItem? node = _sndBuf.First; + while (node is not null) + { + LinkedListNodeOfBufferItem? next = node.Next; + + if (serialNumber == node.ValueRef.Segment.SerialNumber) + { + _sndBuf.Remove(node); + ref KcpBuffer dataRef = ref node.ValueRef.Data; + _sendQueue.SubtractUnflushedBytes(dataRef.Length); + dataRef.Release(); + dataRef = default; + _cache.Return(node); + return true; + } + + if (TimeDiff(serialNumber, node.ValueRef.Segment.SerialNumber) < 0) + { + return false; + } + + node = next; + } + } + + return false; + } + + private bool HandleData(KcpPacketHeader header, ReadOnlySpan data) + { + uint serialNumber = header.SerialNumber; + if (TimeDiff(serialNumber, _rcv_nxt + _rcv_wnd) >= 0 || TimeDiff(serialNumber, _rcv_nxt) < 0) + { + return false; + } + + bool mutated = false; + bool repeat = false; + LinkedListNodeOfBufferItem? node; + lock (_rcvBuf) + { + if (_transportClosed) + { + return false; + } + node = _rcvBuf.Last; + while (node is not null) + { + uint nodeSerialNumber = node.ValueRef.Segment.SerialNumber; + if (serialNumber == nodeSerialNumber) + { + repeat = true; + break; + } + if (TimeDiff(serialNumber, nodeSerialNumber) > 0) + { + break; + } + + node = node.Previous; + } + + if (!repeat) + { + KcpRentedBuffer buffer = _bufferPool.Rent(new KcpBufferPoolRentOptions(data.Length, false)); + KcpSendReceiveBufferItem item = new() + { + Data = KcpBuffer.CreateFromSpan(buffer, data), + Segment = header + }; + if (node is null) + { + _rcvBuf.AddFirst(_cache.Allocate(in item)); + } + else + { + _rcvBuf.AddAfter(node, _cache.Allocate(in item)); + } + mutated = true; + } + + // move available data from rcv_buf -> rcv_queue + node = _rcvBuf.First; + while (node is not null) + { + LinkedListNodeOfBufferItem? next = node.Next; + + if (node.ValueRef.Segment.SerialNumber == _rcv_nxt && _receiveQueue.GetQueueSize() < _rcv_wnd) + { + _rcvBuf.Remove(node); + _receiveQueue.Enqueue(node.ValueRef.Data, node.ValueRef.Segment.Fragment); + node.ValueRef.Data = default; + _cache.Return(node); + _rcv_nxt++; + mutated = true; + } + else + { + break; + } + + node = next; + } + } + + return mutated; + } + + private void AckPush(uint serialNumber, uint timestamp) => _ackList.Add(serialNumber, timestamp); + + private void HandleFastAck(uint serialNumber, uint timestamp) + { + if (TimeDiff(serialNumber, _snd_una) < 0 || TimeDiff(serialNumber, _snd_nxt) >= 0) + { + return; + } + + lock (_sndBuf) + { + LinkedListNodeOfBufferItem? node = _sndBuf.First; + while (node is not null) + { + LinkedListNodeOfBufferItem? next = node.Next; + if (TimeDiff(serialNumber, node.ValueRef.Segment.SerialNumber) < 0) + { + break; + } + else if (serialNumber != node.ValueRef.Segment.SerialNumber) + { + ref KcpSendSegmentStats stats = ref node.ValueRef.Stats; +#if !IKCP_FASTACK_CONSERVE + stats = new KcpSendSegmentStats(stats.ResendTimestamp, stats.Rto, stats.FastAck + 1, stats.TransmitCount); +#else + if (TimeDiff(timestamp, node.ValueRef.Segment.Timestamp) >= 0) + { + stats = new KcpSendSegmentStats(stats.ResendTimestamp, stats.Rto, stats.FastAck + 1, stats.TransmitCount); + } +#endif + } + + node = next; + } + } + } + + private static uint GetTimestamp() => (uint)Environment.TickCount; + + private static int TimeDiff(uint later, uint earlier) => (int)(later - earlier); + + /// + /// Get the size of the next available message in the receive queue. + /// + /// The transport state and the size of the next available message. + /// The receive or peek operation is initiated concurrently. + /// True if the receive queue contains at least one message. False if the receive queue is empty or the transport is closed. + public bool TryPeek(out KcpConversationReceiveResult result) + => _receiveQueue.TryPeek(out result); + + /// + /// Remove the next available message in the receive queue and copy its content into . When in stream mode, move as many bytes as possible into . + /// + /// The buffer to receive message. + /// The transport state and the count of bytes moved into . + /// The size of the next available message is larger than the size of . This exception is never thrown in stream mode. + /// The receive or peek operation is initiated concurrently. + /// True if the next available message is moved into . False if the receive queue is empty or the transport is closed. + public bool TryReceive(Span buffer, out KcpConversationReceiveResult result) + => _receiveQueue.TryReceive(buffer, out result); + + /// + /// Wait until the receive queue contains at least one full message, or at least one byte in stream mode. + /// + /// The token to cancel this operation. + /// The is fired before receive operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when the receive queue contains at least one full message, or at least one byte in stream mode. Its result contains the transport state and the size of the available message. + public ValueTask WaitToReceiveAsync(CancellationToken cancellationToken = default) + => _receiveQueue.WaitToReceiveAsync(cancellationToken); + + /// + /// Wait until the receive queue contains at leat bytes. + /// + /// The minimum bytes in the receive queue. + /// The token to cancel this operation. + /// is a negative integer. + /// The is fired before receive operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when the receive queue contains at least bytes. The result of the task is false when the transport is closed. + public ValueTask WaitForReceiveQueueAvailableDataAsync(int minimumBytes, CancellationToken cancellationToken = default) + => _receiveQueue.WaitForAvailableDataAsync(minimumBytes, 0, cancellationToken); + + /// + /// Wait until the receive queue contains at leat bytes, and also segments. + /// + /// The minimum bytes in the receive queue. + /// The minimum segments in the receive queue + /// The token to cancel this operation. + /// Any od and is a negative integer. + /// The is fired before receive operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when the receive queue contains at least bytes. The result of the task is false when the transport is closed. + public ValueTask WaitForReceiveQueueAvailableDataAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken = default) + => _receiveQueue.WaitForAvailableDataAsync(minimumBytes, minimumSegments, cancellationToken); + + /// + /// Wait for the next full message to arrive if the receive queue is empty. Remove the next available message in the receive queue and copy its content into . When in stream mode, move as many bytes as possible into . + /// + /// The buffer to receive message. + /// The token to cancel this operation. + /// The size of the next available message is larger than the size of . This exception is never thrown in stream mode. + /// The is fired before send operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when a full message is moved into or the transport is closed. Its result contains the transport state and the count of bytes written into . + public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken = default) + => _receiveQueue.ReceiveAsync(buffer, cancellationToken); + + internal ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + => _receiveQueue.ReadAsync(buffer, cancellationToken); + + /// + /// Cancel the current receive operation. + /// + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingReceive() + => _receiveQueue.CancelPendingOperation(null, default); + + /// + /// Cancel the current receive operation. + /// + /// The inner exception of the thrown by the method or method. + /// The in the thrown by the method or method. + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingReceive(Exception? innerException, CancellationToken cancellationToken) + => _receiveQueue.CancelPendingOperation(innerException, cancellationToken); + + /// + public void SetTransportClosed() + { + _transportClosed = true; + Interlocked.Exchange(ref _updateActivation, null)?.Dispose(); + CancellationTokenSource? updateLoopCts = Interlocked.Exchange(ref _updateLoopCts, null); + if (updateLoopCts is not null) + { + updateLoopCts.Cancel(); + updateLoopCts.Dispose(); + } + + _sendQueue.SetTransportClosed(); + _receiveQueue.SetTransportClosed(); + lock (_sndBuf) + { + LinkedListNodeOfBufferItem? node = _sndBuf.First; + LinkedListNodeOfBufferItem? next = node?.Next; + while (node is not null) + { + lock (node) + { + node.ValueRef.Data.Release(); + node.ValueRef = default; + } + + _sndBuf.Remove(node); + node = next; + next = node?.Next; + } + } + lock (_rcvBuf) + { + LinkedListNodeOfBufferItem? node = _rcvBuf.First; + while (node is not null) + { + node.ValueRef.Data.Release(); + node = node.Next; + } + _rcvBuf.Clear(); + } + _queueItemCache.Clear(); + } + + /// + public void Dispose() + { + bool disposed = _disposed; + _disposed = true; + SetTransportClosed(); + if (!disposed) + { + _sendQueue.Dispose(); + _receiveQueue.Dispose(); + } + } + + } +} diff --git a/GameServer/KcpSharp/KcpConversationOptions.cs b/GameServer/KcpSharp/KcpConversationOptions.cs new file mode 100644 index 00000000..f457e764 --- /dev/null +++ b/GameServer/KcpSharp/KcpConversationOptions.cs @@ -0,0 +1,99 @@ +using KcpSharp; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Options used to control the behaviors of . + /// + public class KcpConversationOptions + { + /// + /// The buffer pool to rent buffer from. + /// + public IKcpBufferPool? BufferPool { get; set; } + + /// + /// The maximum packet size that can be transmitted over the underlying transport. + /// + public int Mtu { get; set; } = 1400; + + /// + /// The number of packets in the send window. + /// + public int SendWindow { get; set; } = 32; + + /// + /// The number of packets in the receive window. + /// + public int ReceiveWindow { get; set; } = 128; + + /// + /// The nuber of packets in the receive window of the remote host. + /// + public int RemoteReceiveWindow { get; set; } = 128; + + /// + /// The interval in milliseconds to update the internal state of . + /// + public int UpdateInterval { get; set; } = 100; + + /// + /// Wether no-delay mode is enabled. + /// + public bool NoDelay { get; set; } + + /// + /// The number of ACK packet skipped before a resend is triggered. + /// + public int FastResend { get; set; } + + /// + /// Whether congestion control is disabled. + /// + public bool DisableCongestionControl { get; set; } + + /// + /// Whether stream mode is enabled. + /// + public bool StreamMode { get; set; } + + /// + /// The number of packets in the send queue. + /// + public int SendQueueSize { get; set; } + + /// + /// The number of packets in the receive queue. + /// + public int ReceiveQueueSize { get; set; } + + /// + /// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space. + /// + public int PreBufferSize { get; set; } + + /// + /// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space. + /// + public int PostBufferSize { get; set; } + + /// + /// Options for customized keep-alive functionality. + /// + public KcpKeepAliveOptions? KeepAliveOptions { get; set; } + + /// + /// Options for receive window size notification functionality. + /// + public KcpReceiveWindowNotificationOptions? ReceiveWindowNotificationOptions { get; set; } + + internal const int MtuDefaultValue = 1400; + internal const uint SendWindowDefaultValue = 32; + internal const uint ReceiveWindowDefaultValue = 128; + internal const uint RemoteReceiveWindowDefaultValue = 128; + internal const uint UpdateIntervalDefaultValue = 100; + + internal const int SendQueueSizeDefaultValue = 32; + internal const int ReceiveQueueSizeDefaultValue = 32; + } +} diff --git a/GameServer/KcpSharp/KcpConversationReceiveResult.cs b/GameServer/KcpSharp/KcpConversationReceiveResult.cs new file mode 100644 index 00000000..f91d092d --- /dev/null +++ b/GameServer/KcpSharp/KcpConversationReceiveResult.cs @@ -0,0 +1,61 @@ +using System.Globalization; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// The result of a receive or peek operation. + /// + public readonly struct KcpConversationReceiveResult : IEquatable + { + private readonly int _bytesReceived; + private readonly bool _connectionAlive; + + /// + /// The number of bytes received. + /// + public int BytesReceived => _bytesReceived; + + /// + /// Whether the underlying transport is marked as closed. + /// + public bool TransportClosed => !_connectionAlive; + + /// + /// Construct a with the specified number of bytes received. + /// + /// The number of bytes received. + public KcpConversationReceiveResult(int bytesReceived) + { + _bytesReceived = bytesReceived; + _connectionAlive = true; + } + + /// + /// Checks whether the two instance is equal. + /// + /// The one instance. + /// The other instance. + /// Whether the two instance is equal + public static bool operator ==(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => left.Equals(right); + + /// + /// Checks whether the two instance is not equal. + /// + /// The one instance. + /// The other instance. + /// Whether the two instance is not equal + public static bool operator !=(KcpConversationReceiveResult left, KcpConversationReceiveResult right) => !left.Equals(right); + + /// + public bool Equals(KcpConversationReceiveResult other) => BytesReceived == other.BytesReceived && TransportClosed == other.TransportClosed; + + /// + public override bool Equals(object? obj) => obj is KcpConversationReceiveResult other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(BytesReceived, TransportClosed); + + /// + public override string ToString() => _connectionAlive ? _bytesReceived.ToString(CultureInfo.InvariantCulture) : "Transport is closed."; + } +} diff --git a/GameServer/KcpSharp/KcpConversationUpdateActivation.cs b/GameServer/KcpSharp/KcpConversationUpdateActivation.cs new file mode 100644 index 00000000..6e5ea16a --- /dev/null +++ b/GameServer/KcpSharp/KcpConversationUpdateActivation.cs @@ -0,0 +1,490 @@ +using System.Diagnostics; +using System.Threading.Tasks.Sources; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpConversationUpdateActivation : IValueTaskSource, IDisposable + { + private readonly Timer _timer; + private ManualResetValueTaskSourceCore _mrvtsc; + + private bool _disposed; + private bool _notificationPending; + private bool _signaled; + private bool _activeWait; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + private readonly WaitList _waitList; + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags); + KcpConversationUpdateNotification IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + + try + { + return _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + + lock (this) + { + _signaled = false; + _activeWait = false; + _cancellationRegistration = default; + } + } + } + + public KcpConversationUpdateActivation(int interval) + { + _timer = new Timer(state => + { + WeakReference? reference = (WeakReference?)state!; + if (reference.TryGetTarget(out KcpConversationUpdateActivation? target)) + { + target.Notify(); + } + }, new WeakReference(this), interval, interval); + _mrvtsc = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; + _waitList = new WaitList(this); + } + + public void Notify() + { + if (_disposed) + { + return; + } + lock (this) + { + if (_disposed || _notificationPending) + { + return; + } + if (_activeWait && !_signaled) + { + _signaled = true; + _cancellationToken = default; + _mrvtsc.SetResult(default); + } + else + { + _notificationPending = true; + } + } + } + + private void NotifyPacketReceived() + { + lock (this) + { + if (_disposed) + { + return; + } + if (_activeWait && !_signaled) + { + if (_waitList.Occupy(out KcpConversationUpdateNotification notification)) + { + _signaled = true; + _cancellationToken = default; + bool timerNotification = _notificationPending; + _notificationPending = false; + _mrvtsc.SetResult(notification.WithTimerNotification(timerNotification)); + } + } + } + } + + public ValueTask WaitAsync(CancellationToken cancellationToken) + { + short token; + lock (this) + { + if (_disposed) + { + return default; + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + if (_activeWait) + { + throw new InvalidOperationException(); + } + if (_waitList.Occupy(out KcpConversationUpdateNotification notification)) + { + bool timerNotification = _notificationPending; + _notificationPending = false; + return new ValueTask(notification.WithTimerNotification(timerNotification)); + } + if (_notificationPending) + { + _notificationPending = false; + return default; + } + + _activeWait = true; + Debug.Assert(!_signaled); + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpConversationUpdateActivation?)state)!.CancelWaiting(), this); + return new ValueTask(this, token); + } + + private void CancelWaiting() + { + lock (this) + { + if (_activeWait && !_signaled) + { + CancellationToken cancellationToken = _cancellationToken; + _signaled = true; + _cancellationToken = default; + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + public ValueTask InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) + { + if (_disposed) + { + return default; + } + return _waitList.InputPacketAsync(packet, cancellationToken); + } + + public void Dispose() + { + lock (this) + { + if (_disposed) + { + return; + } + _disposed = true; + if (_activeWait && !_signaled) + { + _signaled = true; + _cancellationToken = default; + _mrvtsc.SetResult(default); + } + } + _timer.Dispose(); + _waitList.Dispose(); + } + + private class WaitList : IValueTaskSource, IKcpConversationUpdateNotificationSource, IDisposable + { + private readonly KcpConversationUpdateActivation _parent; + private LinkedList? _list; + private ManualResetValueTaskSourceCore _mrvtsc; + + private bool _available; // activeWait + private bool _occupied; + private bool _signaled; + private bool _disposed; + + private ReadOnlyMemory _packet; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + public ReadOnlyMemory Packet + { + get + { + lock (this) + { + if (_available && _occupied && !_signaled) + { + return _packet; + } + } + return default; + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags); + void IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + + try + { + _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + + lock (this) + { + _available = false; + _occupied = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + public WaitList(KcpConversationUpdateActivation parent) + { + _parent = parent; + _mrvtsc = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; + } + + public ValueTask InputPacketAsync(ReadOnlyMemory packet, CancellationToken cancellationToken) + { + WaitItem? waitItem = null; + short token = 0; + lock (this) + { + if (_disposed) + { + return default; + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + if (_available) + { + waitItem = new WaitItem(this, packet, cancellationToken); + _list ??= new LinkedList(); + _list.AddLast(waitItem.Node); + } + else + { + token = _mrvtsc.Version; + + _available = true; + Debug.Assert(!_occupied); + Debug.Assert(!_signaled); + _packet = packet; + _cancellationToken = cancellationToken; + } + } + + ValueTask task; + + if (waitItem is null) + { + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((WaitList?)state)!.CancelWaiting(), this); + task = new ValueTask(this, token); + } + else + { + waitItem.RegisterCancellationToken(); + task = new ValueTask(waitItem.Task); + } + + _parent.NotifyPacketReceived(); + + return task; + } + + private void CancelWaiting() + { + lock (this) + { + if (_available && !_occupied && !_signaled) + { + _signaled = true; + CancellationToken cancellationToken = _cancellationToken; + _packet = default; + _cancellationToken = default; + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + public bool Occupy(out KcpConversationUpdateNotification notification) + { + lock (this) + { + if (_disposed) + { + notification = default; + return false; + } + if (_available && !_occupied && !_signaled) + { + _occupied = true; + notification = new KcpConversationUpdateNotification(this, true); + return true; + } + if (_list is null) + { + notification = default; + return false; + } + LinkedListNode? node = _list.First; + if (node is not null) + { + _list.Remove(node); + notification = new KcpConversationUpdateNotification(node.Value, true); + return true; + } + } + notification = default; + return false; + } + + public void Release() + { + lock (this) + { + if (_available && _occupied && !_signaled) + { + _signaled = true; + _packet = default; + _cancellationToken = default; + _mrvtsc.SetResult(true); + } + } + } + + internal bool TryRemove(WaitItem item) + { + lock (this) + { + LinkedList? list = _list; + if (list is null) + { + return false; + } + LinkedListNode node = item.Node; + if (node.Previous is null && node.Next is null) + { + return false; + } + list.Remove(node); + return true; + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + lock (this) + { + _disposed = true; + if (_available && !_occupied && !_signaled) + { + _signaled = true; + _packet = default; + _cancellationToken = default; + _mrvtsc.SetResult(false); + } + + LinkedList? list = _list; + if (list is not null) + { + _list = null; + + LinkedListNode? node = list.First; + LinkedListNode? next = node?.Next; + while (node is not null) + { + node.Value.Release(); + + list.Remove(node); + node = next; + next = node?.Next; + } + } + + } + } + + } + + private class WaitItem : TaskCompletionSource, IKcpConversationUpdateNotificationSource + { + private readonly WaitList _parent; + private ReadOnlyMemory _packet; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + private bool _released; + + public LinkedListNode Node { get; } + + public ReadOnlyMemory Packet + { + get + { + lock (this) + { + if (!_released) + { + return _packet; + } + } + return default; + } + } + + public WaitItem(WaitList parent, ReadOnlyMemory packet, CancellationToken cancellationToken) + { + _parent = parent; + _packet = packet; + _cancellationToken = cancellationToken; + + Node = new LinkedListNode(this); + } + + public void RegisterCancellationToken() + { + _cancellationRegistration = _cancellationToken.UnsafeRegister(state => ((WaitItem?)state)!.CancelWaiting(), this); + } + + private void CancelWaiting() + { + CancellationTokenRegistration cancellationRegistration; + if (_parent.TryRemove(this)) + { + CancellationToken cancellationToken; + lock (this) + { + _released = true; + cancellationToken = _cancellationToken; + cancellationRegistration = _cancellationRegistration; + _packet = default; + _cancellationToken = default; + _cancellationRegistration = default; + } + TrySetCanceled(cancellationToken); + } + _cancellationRegistration.Dispose(); + } + + public void Release() + { + CancellationTokenRegistration cancellationRegistration; + lock (this) + { + _released = true; + cancellationRegistration = _cancellationRegistration; + _packet = default; + _cancellationToken = default; + _cancellationRegistration = default; + } + TrySetResult(); + cancellationRegistration.Dispose(); + } + } + } +} diff --git a/GameServer/KcpSharp/KcpConversationUpdateNotification.cs b/GameServer/KcpSharp/KcpConversationUpdateNotification.cs new file mode 100644 index 00000000..28016a10 --- /dev/null +++ b/GameServer/KcpSharp/KcpConversationUpdateNotification.cs @@ -0,0 +1,30 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal readonly struct KcpConversationUpdateNotification : IDisposable + { + private readonly IKcpConversationUpdateNotificationSource? _source; + private readonly bool _skipTimerNotification; + + public ReadOnlyMemory Packet => _source?.Packet ?? default; + public bool TimerNotification => !_skipTimerNotification; + + public KcpConversationUpdateNotification(IKcpConversationUpdateNotificationSource? source, bool skipTimerNotification) + { + _source = source; + _skipTimerNotification = skipTimerNotification; + } + + public KcpConversationUpdateNotification WithTimerNotification(bool timerNotification) + { + return new KcpConversationUpdateNotification(_source, !_skipTimerNotification | timerNotification); + } + + public void Dispose() + { + if (_source is not null) + { + _source.Release(); + } + } + } +} diff --git a/GameServer/KcpSharp/KcpExceptionProducerExtensions.cs b/GameServer/KcpSharp/KcpExceptionProducerExtensions.cs new file mode 100644 index 00000000..7d817f1e --- /dev/null +++ b/GameServer/KcpSharp/KcpExceptionProducerExtensions.cs @@ -0,0 +1,134 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Helper methods for . + /// + public static class KcpExceptionProducerExtensions + { + /// + /// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation. + /// + /// The producer instance. + /// The exception handler. + public static void SetExceptionHandler(this IKcpExceptionProducer producer, Func handler) + { + if (producer is null) + { + throw new ArgumentNullException(nameof(producer)); + } + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + producer.SetExceptionHandler( + (ex, conv, state) => ((Func?)state)!.Invoke(ex, conv), + handler + ); + } + + /// + /// Set the handler to invoke when exception is thrown. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation. + /// + /// The producer instance. + /// The exception handler. + public static void SetExceptionHandler(this IKcpExceptionProducer producer, Func handler) + { + if (producer is null) + { + throw new ArgumentNullException(nameof(producer)); + } + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + producer.SetExceptionHandler( + (ex, conv, state) => ((Func?)state)!.Invoke(ex), + handler + ); + } + + /// + /// Set the handler to invoke when exception is thrown. + /// + /// The producer instance. + /// The exception handler. + /// The state object to pass into the exception handler. + public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler, object? state) + { + if (producer is null) + { + throw new ArgumentNullException(nameof(producer)); + } + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + producer.SetExceptionHandler( + (ex, conv, state) => + { + var tuple = (Tuple, object?>)state!; + tuple.Item1.Invoke(ex, conv, tuple.Item2); + return false; + }, + Tuple.Create(handler, state) + ); + } + + /// + /// Set the handler to invoke when exception is thrown. + /// + /// The producer instance. + /// The exception handler. + public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler) + { + if (producer is null) + { + throw new ArgumentNullException(nameof(producer)); + } + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + producer.SetExceptionHandler( + (ex, conv, state) => + { + var handler = (Action)state!; + handler.Invoke(ex, conv); + return false; + }, + handler + ); + } + + /// + /// Set the handler to invoke when exception is thrown. + /// + /// The producer instance. + /// The exception handler. + public static void SetExceptionHandler(this IKcpExceptionProducer producer, Action handler) + { + if (producer is null) + { + throw new ArgumentNullException(nameof(producer)); + } + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + producer.SetExceptionHandler( + (ex, conv, state) => + { + var handler = (Action)state!; + handler.Invoke(ex); + return false; + }, + handler + ); + } + } +} diff --git a/GameServer/KcpSharp/KcpGlobalVars.cs b/GameServer/KcpSharp/KcpGlobalVars.cs new file mode 100644 index 00000000..99ed2303 --- /dev/null +++ b/GameServer/KcpSharp/KcpGlobalVars.cs @@ -0,0 +1,14 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal static class KcpGlobalVars + { +#if !CONVID32 + public const ushort CONVID_LENGTH = 8; + public const ushort HEADER_LENGTH_WITH_CONVID = 28; + public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20; +#else + public const ushort HEADER_LENGTH_WITH_CONVID = 24; + public const ushort HEADER_LENGTH_WITHOUT_CONVID = 20; +#endif + } +} diff --git a/GameServer/KcpSharp/KcpKeepAliveOptions.cs b/GameServer/KcpSharp/KcpKeepAliveOptions.cs new file mode 100644 index 00000000..1e7fdda5 --- /dev/null +++ b/GameServer/KcpSharp/KcpKeepAliveOptions.cs @@ -0,0 +1,30 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Options for customized keep-alive functionality. + /// + public sealed class KcpKeepAliveOptions + { + /// + /// Create an instance of option object for customized keep-alive functionality. + /// + /// The minimum interval in milliseconds between sending keep-alive messages. + /// When no packets are received during this period (in milliseconds), the transport is considered to be closed. + public KcpKeepAliveOptions(int sendInterval, int gracePeriod) + { + if (sendInterval <= 0) + { + throw new ArgumentOutOfRangeException(nameof(sendInterval)); + } + if (gracePeriod <= 0) + { + throw new ArgumentOutOfRangeException(nameof(gracePeriod)); + } + SendInterval = sendInterval; + GracePeriod = gracePeriod; + } + + internal int SendInterval { get; } + internal int GracePeriod { get; } + } +} diff --git a/GameServer/KcpSharp/KcpMultiplexConnection.cs b/GameServer/KcpSharp/KcpMultiplexConnection.cs new file mode 100644 index 00000000..6cf2a09d --- /dev/null +++ b/GameServer/KcpSharp/KcpMultiplexConnection.cs @@ -0,0 +1,335 @@ +using KcpSharp; +using System.Buffers.Binary; +using System.Collections.Concurrent; +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Multiplex many channels or conversations over the same transport. + /// + /// The state of the channel. + public sealed class KcpMultiplexConnection : IKcpTransport, IKcpConversation, IKcpMultiplexConnection + { + private readonly IKcpTransport _transport; + + private readonly ConcurrentDictionary _conversations = new(); + private bool _transportClosed; + private bool _disposed; + + private readonly Action? _disposeAction; + + /// + /// Construct a multiplexed connection over a transport. + /// + /// The underlying transport. + public KcpMultiplexConnection(IKcpTransport transport) + { + _transport = transport ?? throw new ArgumentNullException(nameof(transport)); + _disposeAction = null; + } + + /// + /// Construct a multiplexed connection over a transport. + /// + /// The underlying transport. + /// The action to invoke when state object is removed. + public KcpMultiplexConnection(IKcpTransport transport, Action? disposeAction) + { + _transport = transport ?? throw new ArgumentNullException(nameof(transport)); + _disposeAction = disposeAction; + } + + private void CheckDispose() + { + if (_disposed) + { + ThrowObjectDisposedException(); + } + } + + private static void ThrowObjectDisposedException() + { + throw new ObjectDisposedException(nameof(KcpMultiplexConnection)); + } + + /// + /// Process a newly received packet from the transport. + /// + /// The content of the packet with conversation ID. + /// A token to cancel this operation. + /// A that completes when the packet is handled by the corresponding channel or conversation. + public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default) + { + ReadOnlySpan span = packet.Buffer.AsSpan(); + if (span.Length < KcpGlobalVars.CONVID_LENGTH) + { + return default; + } + if (_transportClosed || _disposed) + { + return default; + } + long id = BinaryPrimitives.ReadInt64BigEndian(span); + if (_conversations.TryGetValue(id, out (IKcpConversation Conversation, T? State) value)) + { + return value.Conversation.InputPakcetAsync(packet, cancellationToken); + } + return default; + } + + /// + /// Determine whether the multiplex connection contains a conversation with the specified id. + /// + /// The conversation ID. + /// True if the multiplex connection contains the specified conversation. Otherwise false. + public bool Contains(long id) + { + CheckDispose(); + return _conversations.ContainsKey(id); + } + + /// + /// Create a raw channel with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The options of the . + /// The raw channel created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, KcpRawChannelOptions? options = null) + { + KcpRawChannel? channel = new(remoteEndpoint, this, id, options); + try + { + RegisterConversation(channel, id, default); + if (_transportClosed) + { + channel.SetTransportClosed(); + } + return Interlocked.Exchange(ref channel, null)!; + } + finally + { + if (channel is not null) + { + channel.Dispose(); + } + } + } + + /// + /// Create a raw channel with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The user state of this channel. + /// The options of the . + /// The raw channel created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public KcpRawChannel CreateRawChannel(long id, IPEndPoint remoteEndpoint, T state, KcpRawChannelOptions? options = null) + { + KcpRawChannel? channel = new(remoteEndpoint, this, id, options); + try + { + RegisterConversation(channel, id, state); + if (_transportClosed) + { + channel.SetTransportClosed(); + } + return Interlocked.Exchange(ref channel, null)!; + } + finally + { + if (channel is not null) + { + channel.Dispose(); + } + } + } + + /// + /// Create a conversation with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The options of the . + /// The KCP conversation created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, KcpConversationOptions? options = null) + { + KcpConversation? conversation = new(remoteEndpoint, this, id, options); + try + { + RegisterConversation(conversation, id, default); + if (_transportClosed) + { + conversation.SetTransportClosed(); + } + return Interlocked.Exchange(ref conversation, null)!; + } + finally + { + if (conversation is not null) + { + conversation.Dispose(); + } + } + } + + /// + /// Create a conversation with the specified conversation ID. + /// + /// The conversation ID. + /// The remote Endpoint + /// The user state of this conversation. + /// The options of the . + /// The KCP conversation created. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public KcpConversation CreateConversation(long id, IPEndPoint remoteEndpoint, T state, KcpConversationOptions? options = null) + { + KcpConversation? conversation = new(remoteEndpoint, this, id, options); + try + { + RegisterConversation(conversation, id, state); + if (_transportClosed) + { + conversation.SetTransportClosed(); + } + return Interlocked.Exchange(ref conversation, null)!; + } + finally + { + if (conversation is not null) + { + conversation.Dispose(); + } + } + } + + /// + /// Register a conversation or channel with the specified conversation ID and user state. + /// + /// The conversation or channel to register. + /// The conversation ID. + /// is not provided. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public void RegisterConversation(IKcpConversation conversation, long id) + => RegisterConversation(conversation, id, default); + + /// + /// Register a conversation or channel with the specified conversation ID and user state. + /// + /// The conversation or channel to register. + /// The conversation ID. + /// The user state + /// is not provided. + /// The current instance is disposed. + /// Another channel or conversation with the same ID was already registered. + public void RegisterConversation(IKcpConversation conversation, long id, T? state) + { + if (conversation is null) + { + throw new ArgumentNullException(nameof(conversation)); + } + + CheckDispose(); + (IKcpConversation addedConversation, T? _) = _conversations.GetOrAdd(id, (conversation, state)); + if (!ReferenceEquals(addedConversation, conversation)) + { + throw new InvalidOperationException("Duplicated conversation."); + } + if (_disposed) + { + if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value) && _disposeAction is not null) + { + _disposeAction.Invoke(value.State); + } + ThrowObjectDisposedException(); + } + } + + /// + /// Unregister a conversation or channel with the specified conversation ID. + /// + /// The conversation ID. + /// The conversation unregistered. Returns null when the conversation with the specified ID is not found. + public IKcpConversation? UnregisterConversation(long id) + { + return UnregisterConversation(id, out _); + } + + /// + /// Unregister a conversation or channel with the specified conversation ID. + /// + /// The conversation ID. + /// The user state. + /// The conversation unregistered. Returns null when the conversation with the specified ID is not found. + public IKcpConversation? UnregisterConversation(long id, out T? state) + { + if (!_transportClosed && !_disposed && _conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value)) + { + value.Conversation.SetTransportClosed(); + state = value.State; + if (_disposeAction is not null) + { + _disposeAction.Invoke(state); + } + return value.Conversation; + } + state = default; + return default; + } + + /// + public ValueTask SendPacketAsync(Memory packet, IPEndPoint remoteEndpoint, CancellationToken cancellationToken = default) + { + if (_transportClosed || _disposed) + { + return default; + } + return _transport.SendPacketAsync(packet, remoteEndpoint, cancellationToken); + } + + /// + public void SetTransportClosed() + { + _transportClosed = true; + foreach ((IKcpConversation conversation, T? _) in _conversations.Values) + { + conversation.SetTransportClosed(); + } + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + _transportClosed = true; + _disposed = true; + while (!_conversations.IsEmpty) + { + foreach (long id in _conversations.Keys) + { + if (_conversations.TryRemove(id, out (IKcpConversation Conversation, T? State) value)) + { + value.Conversation.Dispose(); + if (_disposeAction is not null) + { + _disposeAction.Invoke(value.State); + } + } + } + } + } + } +} diff --git a/GameServer/KcpSharp/KcpPacketHeader.cs b/GameServer/KcpSharp/KcpPacketHeader.cs new file mode 100644 index 00000000..5ecc8602 --- /dev/null +++ b/GameServer/KcpSharp/KcpPacketHeader.cs @@ -0,0 +1,75 @@ +using System.Buffers.Binary; +using System.Diagnostics; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal readonly struct KcpPacketHeader : IEquatable + { + public KcpPacketHeader(KcpCommand command, byte fragment, ushort windowSize, uint timestamp, uint serialNumber, uint unacknowledged) + { + Command = command; + Fragment = fragment; + WindowSize = windowSize; + Timestamp = timestamp; + SerialNumber = serialNumber; + Unacknowledged = unacknowledged; + } + + internal KcpPacketHeader(byte fragment) + { + Command = 0; + Fragment = fragment; + WindowSize = 0; + Timestamp = 0; + SerialNumber = 0; + Unacknowledged = 0; + } + + public KcpCommand Command { get; } + public byte Fragment { get; } + public ushort WindowSize { get; } + public uint Timestamp { get; } + public uint SerialNumber { get; } + public uint Unacknowledged { get; } + + public bool Equals(KcpPacketHeader other) => Command == other.Command && Fragment == other.Fragment && WindowSize == other.WindowSize && Timestamp == other.Timestamp && SerialNumber == other.SerialNumber && Unacknowledged == other.Unacknowledged; + public override bool Equals(object? obj) => obj is KcpPacketHeader other && Equals(other); + public override int GetHashCode() => HashCode.Combine(Command, Fragment, WindowSize, Timestamp, SerialNumber, Unacknowledged); + + public static KcpPacketHeader Parse(ReadOnlySpan buffer) + { + Debug.Assert(buffer.Length >= 16); + return new KcpPacketHeader( + (KcpCommand)buffer[0], + buffer[1], + BinaryPrimitives.ReadUInt16LittleEndian(buffer.Slice(2)), + BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(4)), + BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8)), + BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(12)) + ); + } + + internal void EncodeHeader(ulong? conversationId, int payloadLength, Span destination, out int bytesWritten) + { + Debug.Assert(destination.Length >= 20); + if (conversationId.HasValue) + { + BinaryPrimitives.WriteUInt64BigEndian(destination, conversationId.GetValueOrDefault()); + destination = destination.Slice(8); + bytesWritten = 28; + } + else + { + bytesWritten = 20; + } + Debug.Assert(destination.Length >= 20); + destination[1] = Fragment; + destination[0] = (byte)Command; + BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(2), WindowSize); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(4), Timestamp); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(8), SerialNumber); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(12), Unacknowledged); + BinaryPrimitives.WriteUInt32LittleEndian(destination.Slice(16), (uint)payloadLength); + } + } +} diff --git a/GameServer/KcpSharp/KcpProbeType.cs b/GameServer/KcpSharp/KcpProbeType.cs new file mode 100644 index 00000000..34b28c2a --- /dev/null +++ b/GameServer/KcpSharp/KcpProbeType.cs @@ -0,0 +1,10 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + [Flags] + internal enum KcpProbeType + { + None = 0, + AskSend = 1, + AskTell = 2, + } +} diff --git a/GameServer/KcpSharp/KcpRawChannel.cs b/GameServer/KcpSharp/KcpRawChannel.cs new file mode 100644 index 00000000..2e1101f9 --- /dev/null +++ b/GameServer/KcpSharp/KcpRawChannel.cs @@ -0,0 +1,372 @@ +using KcpSharp; +using System.Buffers.Binary; +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// An unreliable channel with a conversation ID. + /// + public sealed class KcpRawChannel : IKcpConversation, IKcpExceptionProducer + { + private readonly IKcpBufferPool _bufferPool; + private readonly IKcpTransport _transport; + private readonly IPEndPoint _remoteEndPoint; + private readonly ulong? _id; + private readonly int _mtu; + private readonly int _preBufferSize; + private readonly int _postBufferSize; + + private CancellationTokenSource? _sendLoopCts; + private readonly KcpRawReceiveQueue _receiveQueue; + private readonly KcpRawSendOperation _sendOperation; + private readonly AsyncAutoResetEvent _sendNotification; + + private Func? _exceptionHandler; + private object? _exceptionHandlerState; + + /// + /// Construct a unreliable channel with a conversation ID. + /// + /// The remote Endpoint + /// The underlying transport. + /// The options of the . + public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, KcpRawChannelOptions? options) + : this(remoteEndPoint, transport, null, options) + { } + + /// + /// Construct a unreliable channel with a conversation ID. + /// + /// The remote Endpoint + /// The underlying transport. + /// The conversation ID. + /// The options of the . + public KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, long conversationId, KcpRawChannelOptions? options) + : this(remoteEndPoint, transport, (ulong)conversationId, options) + { } + + private KcpRawChannel(IPEndPoint remoteEndPoint, IKcpTransport transport, ulong? conversationId, KcpRawChannelOptions? options) + { + _bufferPool = options?.BufferPool ?? DefaultArrayPoolBufferAllocator.Default; + _remoteEndPoint = remoteEndPoint; + _transport = transport; + _id = conversationId; + + if (options is null) + { + _mtu = KcpConversationOptions.MtuDefaultValue; + } + else if (options.Mtu < 50) + { + throw new ArgumentException("MTU must be at least 50.", nameof(options)); + } + else + { + _mtu = options.Mtu; + } + + _preBufferSize = options?.PreBufferSize ?? 0; + _postBufferSize = options?.PostBufferSize ?? 0; + if (_preBufferSize < 0) + { + throw new ArgumentException("PreBufferSize must be a non-negative integer.", nameof(options)); + } + if (_postBufferSize < 0) + { + throw new ArgumentException("PostBufferSize must be a non-negative integer.", nameof(options)); + } + if ((uint)(_preBufferSize + _postBufferSize) >= (uint)_mtu) + { + throw new ArgumentException("The sum of PreBufferSize and PostBufferSize must be less than MTU.", nameof(options)); + } + if (conversationId.HasValue && (uint)(_preBufferSize + _postBufferSize) >= (uint)(_mtu - 4)) + { + throw new ArgumentException("The sum of PreBufferSize and PostBufferSize is too large. There is not enough space in the packet for the conversation ID.", nameof(options)); + } + + int queueSize = options?.ReceiveQueueSize ?? 32; + if (queueSize < 1) + { + throw new ArgumentException("QueueSize must be a positive integer.", nameof(options)); + } + + _sendLoopCts = new CancellationTokenSource(); + _sendNotification = new AsyncAutoResetEvent(); + _receiveQueue = new KcpRawReceiveQueue(_bufferPool, queueSize); + _sendOperation = new KcpRawSendOperation(_sendNotification); + + RunSendLoop(); + } + + /// + /// Set the handler to invoke when exception is thrown during flushing packets to the transport. Return true in the handler to ignore the error and continue running. Return false in the handler to abort the operation and mark the transport as closed. + /// + /// The exception handler. + /// The state object to pass into the exception handler. + public void SetExceptionHandler(Func handler, object? state) + { + if (handler is null) + { + throw new ArgumentNullException(nameof(handler)); + } + + _exceptionHandler = handler; + _exceptionHandlerState = state; + } + + /// + /// Get the ID of the current conversation. + /// + public long? ConversationId => (long?)_id; + + /// + /// Get whether the transport is marked as closed. + /// + public bool TransportClosed => _sendLoopCts is null; + + /// + /// Send message to the underlying transport. + /// + /// The content of the message + /// The token to cancel this operation. + /// The size of the message is larger than mtu, thus it can not be sent. + /// The is fired before send operation is completed. + /// The send operation is initiated concurrently. + /// The instance is disposed. + /// A that completes when the entire message is put into the queue. The result of the task is false when the transport is closed. + public ValueTask SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + => _sendOperation.SendAsync(buffer, cancellationToken); + + + /// + /// Cancel the current send operation or flush operation. + /// + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingSend() + => _sendOperation.CancelPendingOperation(null, default); + + /// + /// Cancel the current send operation or flush operation. + /// + /// The inner exception of the thrown by the method. + /// The in the thrown by the method. + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingSend(Exception? innerException, CancellationToken cancellationToken) + => _sendOperation.CancelPendingOperation(innerException, cancellationToken); + + + private async void RunSendLoop() + { + CancellationToken cancellationToken = _sendLoopCts?.Token ?? new CancellationToken(true); + KcpRawSendOperation sendOperation = _sendOperation; + AsyncAutoResetEvent ev = _sendNotification; + int mss = _mtu - _preBufferSize - _postBufferSize; + if (_id.HasValue) + { + mss -= 8; + } + + try + { + while (!cancellationToken.IsCancellationRequested) + { + int payloadSize = await ev.WaitAsync().ConfigureAwait(false); + if (cancellationToken.IsCancellationRequested) + { + break; + } + + if (payloadSize < 0 || payloadSize > mss) + { + _ = sendOperation.TryConsume(default, out _); + continue; + } + + int overhead = _preBufferSize + _postBufferSize; + if (_id.HasValue) + { + overhead += 8; + } + { + using KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(payloadSize + overhead, true)); + Memory memory = owner.Memory; + + // Fill the buffer + if (_preBufferSize != 0) + { + memory.Span.Slice(0, _preBufferSize).Clear(); + memory = memory.Slice(_preBufferSize); + } + if (_id.HasValue) + { + BinaryPrimitives.WriteUInt64LittleEndian(memory.Span, _id.GetValueOrDefault()); + memory = memory.Slice(8); + } + if (!sendOperation.TryConsume(memory, out int bytesWritten)) + { + continue; + } + payloadSize = Math.Min(payloadSize, bytesWritten); + memory = memory.Slice(payloadSize); + if (_postBufferSize != 0) + { + memory.Span.Slice(0, _postBufferSize).Clear(); + } + + // Send the buffer + try + { + await _transport.SendPacketAsync(owner.Memory.Slice(0, payloadSize + overhead), _remoteEndPoint, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + if (!HandleFlushException(ex)) + { + break; + } + } + } + } + } + catch (OperationCanceledException) + { + // Do nothing + } + catch (Exception ex) + { + HandleFlushException(ex); + } + } + + + private bool HandleFlushException(Exception ex) + { + Func? handler = _exceptionHandler; + object? state = _exceptionHandlerState; + bool result = false; + if (handler is not null) + { + try + { + result = handler.Invoke(ex, this, state); + } + catch + { + result = false; + } + } + + if (!result) + { + SetTransportClosed(); + } + return result; + } + + /// + public ValueTask InputPakcetAsync(UdpReceiveResult packet, CancellationToken cancellationToken = default) + { + ReadOnlySpan span = packet.Buffer.AsSpan(); + int overhead = _id.HasValue ? KcpGlobalVars.CONVID_LENGTH : 0; + if (span.Length < overhead || span.Length > _mtu) + { + return default; + } + if (_id.HasValue) + { + if (BinaryPrimitives.ReadUInt64BigEndian(span) != _id.GetValueOrDefault()) + { + return default; + } + span = span.Slice(8); + } + _receiveQueue.Enqueue(span); + return default; + } + + /// + /// Get the size of the next available message in the receive queue. + /// + /// The transport state and the size of the next available message. + /// The receive or peek operation is initiated concurrently. + /// True if the receive queue contains at least one message. False if the receive queue is empty or the transport is closed. + public bool TryPeek(out KcpConversationReceiveResult result) + => _receiveQueue.TryPeek(out result); + + /// + /// Remove the next available message in the receive queue and copy its content into . + /// + /// The buffer to receive message. + /// The transport state and the count of bytes moved into . + /// The size of the next available message is larger than the size of . + /// The receive or peek operation is initiated concurrently. + /// True if the next available message is moved into . False if the receive queue is empty or the transport is closed. + public bool TryReceive(Span buffer, out KcpConversationReceiveResult result) + => _receiveQueue.TryReceive(buffer, out result); + + /// + /// Wait until the receive queue contains at least one message. + /// + /// The token to cancel this operation. + /// The is fired before receive operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when the receive queue contains at least one full message, or at least one byte in stream mode. Its result contains the transport state and the size of the available message. + public ValueTask WaitToReceiveAsync(CancellationToken cancellationToken) + => _receiveQueue.WaitToReceiveAsync(cancellationToken); + + /// + /// Wait for the next full message to arrive if the receive queue is empty. Remove the next available message in the receive queue and copy its content into . + /// + /// The buffer to receive message. + /// The token to cancel this operation. + /// The size of the next available message is larger than the size of . + /// The is fired before send operation is completed. + /// The receive or peek operation is initiated concurrently. + /// A that completes when a message is moved into or the transport is closed. Its result contains the transport state and the count of bytes written into . + public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken = default) + => _receiveQueue.ReceiveAsync(buffer, cancellationToken); + + + /// + /// Cancel the current receive operation. + /// + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingReceive() + => _receiveQueue.CancelPendingOperation(null, default); + + /// + /// Cancel the current send operation or flush operation. + /// + /// The inner exception of the thrown by the method or method. + /// The in the thrown by the method or method. + /// True if the current operation is canceled. False if there is no active send operation. + public bool CancelPendingReceive(Exception? innerException, CancellationToken cancellationToken) + => _receiveQueue.CancelPendingOperation(innerException, cancellationToken); + + + /// + public void SetTransportClosed() + { + CancellationTokenSource? cts = Interlocked.Exchange(ref _sendLoopCts, null); + if (cts is not null) + { + cts.Cancel(); + cts.Dispose(); + } + + _receiveQueue.SetTransportClosed(); + _sendOperation.SetTransportClosed(); + _sendNotification.Set(0); + } + + /// + public void Dispose() + { + SetTransportClosed(); + _receiveQueue.Dispose(); + _sendOperation.Dispose(); + } + } +} diff --git a/GameServer/KcpSharp/KcpRawChannelOptions.cs b/GameServer/KcpSharp/KcpRawChannelOptions.cs new file mode 100644 index 00000000..5a5fc92a --- /dev/null +++ b/GameServer/KcpSharp/KcpRawChannelOptions.cs @@ -0,0 +1,33 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Options used to control the behaviors of . + /// + public sealed class KcpRawChannelOptions + { + /// + /// The buffer pool to rent buffer from. + /// + public IKcpBufferPool? BufferPool { get; set; } + + /// + /// The maximum packet size that can be transmitted over the underlying transport. + /// + public int Mtu { get; set; } = 1400; + + /// + /// The number of packets in the receive queue. + /// + public int ReceiveQueueSize { get; set; } = 32; + + /// + /// The number of bytes to reserve at the start of buffer passed into the underlying transport. The transport should fill this reserved space. + /// + public int PreBufferSize { get; set; } + + /// + /// The number of bytes to reserve at the end of buffer passed into the underlying transport. The transport should fill this reserved space. + /// + public int PostBufferSize { get; set; } + } +} diff --git a/GameServer/KcpSharp/KcpRawReceiveQueue.cs b/GameServer/KcpSharp/KcpRawReceiveQueue.cs new file mode 100644 index 00000000..1b2c5c15 --- /dev/null +++ b/GameServer/KcpSharp/KcpRawReceiveQueue.cs @@ -0,0 +1,358 @@ +using System.Threading.Tasks.Sources; +using System.Diagnostics; + + + + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList; +using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode; +#else +using LinkedListOfQueueItem = System.Collections.Generic.LinkedList; +using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode; +#endif + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpRawReceiveQueue : IValueTaskSource, IDisposable + { + private ManualResetValueTaskSourceCore _mrvtsc; + + private readonly IKcpBufferPool _bufferPool; + private readonly int _capacity; + private readonly LinkedListOfQueueItem _queue; + private readonly LinkedListOfQueueItem _recycled; + + private bool _transportClosed; + private bool _disposed; + + private bool _activeWait; + private bool _signaled; + private bool _bufferProvided; + private Memory _buffer; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + public KcpRawReceiveQueue(IKcpBufferPool bufferPool, int capacity) + { + _bufferPool = bufferPool; + _capacity = capacity; + _queue = new LinkedListOfQueueItem(); + _recycled = new LinkedListOfQueueItem(); + } + + KcpConversationReceiveResult IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags); + + public bool TryPeek(out KcpConversationReceiveResult result) + { + lock (_queue) + { + if (_disposed || _transportClosed) + { + result = default; + return false; + } + if (_activeWait) + { + ThrowHelper.ThrowConcurrentReceiveException(); + } + LinkedListNodeOfQueueItem? first = _queue.First; + if (first is null) + { + result = new KcpConversationReceiveResult(0); + return false; + } + + result = new KcpConversationReceiveResult(first.ValueRef.Length); + return true; + } + } + + public ValueTask WaitToReceiveAsync(CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return default; + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + LinkedListNodeOfQueueItem? first = _queue.First; + if (first is not null) + { + return new ValueTask(new KcpConversationReceiveResult(first.ValueRef.Length)); + } + + _activeWait = true; + Debug.Assert(!_signaled); + _bufferProvided = false; + _buffer = default; + _cancellationToken = cancellationToken; + + token = _mrvtsc.Version; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool TryReceive(Span buffer, out KcpConversationReceiveResult result) + { + lock (_queue) + { + if (_disposed || _transportClosed) + { + result = default; + return false; + } + if (_activeWait) + { + ThrowHelper.ThrowConcurrentReceiveException(); + } + LinkedListNodeOfQueueItem? first = _queue.First; + if (first is null) + { + result = new KcpConversationReceiveResult(0); + return false; + } + + ref KcpBuffer source = ref first.ValueRef; + if (buffer.Length < source.Length) + { + ThrowHelper.ThrowBufferTooSmall(); + } + + source.DataRegion.Span.CopyTo(buffer); + result = new KcpConversationReceiveResult(source.Length); + + _queue.RemoveFirst(); + source.Release(); + source = default; + _recycled.AddLast(first); + + return true; + } + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken = default) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return default; + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + LinkedListNodeOfQueueItem? first = _queue.First; + if (first is not null) + { + ref KcpBuffer source = ref first.ValueRef; + int length = source.Length; + if (buffer.Length < source.Length) + { + return new ValueTask(Task.FromException(ThrowHelper.NewBufferTooSmallForBufferArgument())); + } + _queue.Remove(first); + + source.DataRegion.CopyTo(buffer); + source.Release(); + source = default; + _recycled.AddLast(first); + + return new ValueTask(new KcpConversationReceiveResult(length)); + } + + _activeWait = true; + Debug.Assert(!_signaled); + _bufferProvided = true; + _buffer = buffer; + _cancellationToken = cancellationToken; + + token = _mrvtsc.Version; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken) + { + lock (_queue) + { + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException, cancellationToken)); + return true; + } + } + return false; + } + + private void SetCanceled() + { + lock (_queue) + { + if (_activeWait && !_signaled) + { + CancellationToken cancellationToken = _cancellationToken; + ClearPreviousOperation(); + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + private void ClearPreviousOperation() + { + _signaled = true; + _bufferProvided = false; + _buffer = default; + _cancellationToken = default; + } + + public void Enqueue(ReadOnlySpan buffer) + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + + int queueSize = _queue.Count; + if (queueSize > 0 || !_activeWait) + { + if (queueSize >= _capacity) + { + return; + } + + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false)); + _queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer))); + return; + } + + if (!_bufferProvided) + { + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false)); + _queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer))); + + ClearPreviousOperation(); + _mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length)); + return; + } + + if (buffer.Length > _buffer.Length) + { + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(buffer.Length, false)); + _queue.AddLast(AllocateNode(KcpBuffer.CreateFromSpan(owner, buffer))); + + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument()); + return; + } + + buffer.CopyTo(_buffer.Span); + ClearPreviousOperation(); + _mrvtsc.SetResult(new KcpConversationReceiveResult(buffer.Length)); + } + } + + private LinkedListNodeOfQueueItem AllocateNode(KcpBuffer buffer) + { + LinkedListNodeOfQueueItem? node = _recycled.First; + if (node is null) + { + node = new LinkedListNodeOfQueueItem(buffer); + } + else + { + node.ValueRef = buffer; + _recycled.Remove(node); + } + return node; + } + + public void SetTransportClosed() + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(default); + } + _recycled.Clear(); + _transportClosed = true; + } + } + + public void Dispose() + { + lock (_queue) + { + if (_disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(default); + } + LinkedListNodeOfQueueItem? node = _queue.First; + while (node is not null) + { + node.ValueRef.Release(); + node = node.Next; + } + _queue.Clear(); + _recycled.Clear(); + _disposed = true; + _transportClosed = true; + } + } + } +} diff --git a/GameServer/KcpSharp/KcpRawSendOperation.cs b/GameServer/KcpSharp/KcpRawSendOperation.cs new file mode 100644 index 00000000..3a930c52 --- /dev/null +++ b/GameServer/KcpSharp/KcpRawSendOperation.cs @@ -0,0 +1,182 @@ +using System.Diagnostics; +using System.Threading.Tasks.Sources; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpRawSendOperation : IValueTaskSource, IDisposable + { + private readonly AsyncAutoResetEvent _notification; + private ManualResetValueTaskSourceCore _mrvtsc; + + private bool _transportClosed; + private bool _disposed; + + private bool _activeWait; + private bool _signaled; + private ReadOnlyMemory _buffer; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + public KcpRawSendOperation(AsyncAutoResetEvent notification) + { + _notification = notification; + + _mrvtsc = new ManualResetValueTaskSourceCore() + { + RunContinuationsAsynchronously = true + }; + } + + bool IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + lock (this) + { + _activeWait = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) => _mrvtsc.OnCompleted(continuation, state, token, flags); + + public ValueTask SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + short token; + lock (this) + { + if (_transportClosed || _disposed) + { + return new ValueTask(false); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _activeWait = true; + Debug.Assert(!_signaled); + _buffer = buffer; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpRawSendOperation?)state)!.SetCanceled(), this); + + _notification.Set(buffer.Length); + return new ValueTask(this, token); + } + + public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken) + { + lock (this) + { + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken)); + return true; + } + } + return false; + } + + private void SetCanceled() + { + lock (this) + { + if (_activeWait && !_signaled) + { + CancellationToken cancellationToken = _cancellationToken; + ClearPreviousOperation(); + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + private void ClearPreviousOperation() + { + _signaled = true; + _buffer = default; + _cancellationToken = default; + } + + public bool TryConsume(Memory buffer, out int bytesWritten) + { + lock (this) + { + if (_transportClosed || _disposed) + { + bytesWritten = 0; + return false; + } + if (!_activeWait) + { + bytesWritten = 0; + return false; + } + ReadOnlyMemory source = _buffer; + if (source.Length > buffer.Length) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewMessageTooLargeForBufferArgument()); + bytesWritten = 0; + return false; + } + source.CopyTo(buffer); + bytesWritten = source.Length; + ClearPreviousOperation(); + _mrvtsc.SetResult(true); + return true; + } + } + + public void SetTransportClosed() + { + lock (this) + { + if (_transportClosed || _disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(false); + } + _transportClosed = true; + } + } + + public void Dispose() + { + lock (this) + { + if (_disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(false); + } + _disposed = true; + _transportClosed = true; + } + } + } +} diff --git a/GameServer/KcpSharp/KcpReceiveQueue.cs b/GameServer/KcpSharp/KcpReceiveQueue.cs new file mode 100644 index 00000000..c5858640 --- /dev/null +++ b/GameServer/KcpSharp/KcpReceiveQueue.cs @@ -0,0 +1,698 @@ +using System.Threading.Tasks.Sources; +using System.Diagnostics; + + + + + + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>; +#else +using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +#endif + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpReceiveQueue : IValueTaskSource, IValueTaskSource, IValueTaskSource, IDisposable + { + private ManualResetValueTaskSourceCore _mrvtsc; + + private readonly LinkedListOfQueueItem _queue; + private readonly bool _stream; + private readonly int _queueSize; + private readonly KcpSendReceiveQueueItemCache _cache; + private int _completedPacketsCount; + + private bool _transportClosed; + private bool _disposed; + + private bool _activeWait; + private bool _signaled; + private byte _operationMode; // 0-receive 1-wait for message 2-wait for available data + private Memory _buffer; + private int _minimumBytes; + private int _minimumSegments; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + public KcpReceiveQueue(bool stream, int queueSize, KcpSendReceiveQueueItemCache cache) + { + _mrvtsc = new ManualResetValueTaskSourceCore() + { + RunContinuationsAsynchronously = true + }; + _queue = new LinkedListOfQueueItem(); + _stream = stream; + _queueSize = queueSize; + _cache = cache; + } + + public ValueTaskSourceStatus GetStatus(short token) => _mrvtsc.GetStatus(token); + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => _mrvtsc.OnCompleted(continuation, state, token, flags); + + KcpConversationReceiveResult IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + int IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return _mrvtsc.GetResult(token).BytesReceived; + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + bool IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return !_mrvtsc.GetResult(token).TransportClosed; + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signaled = false; + _cancellationRegistration = default; + } + } + } + + public bool TryPeek(out KcpConversationReceiveResult result) + { + lock (_queue) + { + if (_disposed || _transportClosed) + { + result = default; + return false; + } + if (_activeWait) + { + ThrowHelper.ThrowConcurrentReceiveException(); + } + + if (_completedPacketsCount == 0) + { + result = new KcpConversationReceiveResult(0); + return false; + } + + LinkedListNodeOfQueueItem? node = _queue.First; + if (node is null) + { + result = new KcpConversationReceiveResult(0); + return false; + } + + if (CalculatePacketSize(node, out int packetSize)) + { + result = new KcpConversationReceiveResult(packetSize); + return true; + } + + result = default; + return false; + } + } + + public ValueTask WaitToReceiveAsync(CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return default; + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _operationMode = 1; + _buffer = default; + _minimumBytes = 0; + _minimumSegments = 0; + + token = _mrvtsc.Version; + if (_completedPacketsCount > 0) + { + ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall); + ClearPreviousOperation(false); + if (bufferTooSmall) + { + Debug.Assert(false, "This should never be reached."); + return new ValueTask(Task.FromException(ThrowHelper.NewBufferTooSmallForBufferArgument())); + } + else + { + return new ValueTask(result); + } + } + + _activeWait = true; + Debug.Assert(!_signaled); + _cancellationToken = cancellationToken; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public ValueTask WaitForAvailableDataAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken) + { + if (minimumBytes < 0) + { + return new ValueTask(Task.FromException(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes)))); + } + if (minimumSegments < 0) + { + return new ValueTask(Task.FromException(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments)))); + } + + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return default; + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + if (CheckQueeuSize(_queue, minimumBytes, minimumSegments, _stream)) + { + return new ValueTask(true); + } + + _activeWait = true; + Debug.Assert(!_signaled); + _operationMode = 2; + _buffer = default; + _minimumBytes = minimumBytes; + _minimumSegments = minimumSegments; + _cancellationToken = cancellationToken; + + token = _mrvtsc.Version; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool TryReceive(Span buffer, out KcpConversationReceiveResult result) + { + lock (_queue) + { + if (_disposed || _transportClosed) + { + result = default; + return false; + } + if (_activeWait) + { + ThrowHelper.ThrowConcurrentReceiveException(); + } + + if (_completedPacketsCount == 0) + { + result = new KcpConversationReceiveResult(0); + return false; + } + + Debug.Assert(!_signaled); + _operationMode = 0; + + ConsumePacket(buffer, out result, out bool bufferTooSmall); + ClearPreviousOperation(false); + if (bufferTooSmall) + { + ThrowHelper.ThrowBufferTooSmall(); + } + return true; + } + } + + public ValueTask ReceiveAsync(Memory buffer, CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return default; + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _operationMode = 0; + _buffer = buffer; + + token = _mrvtsc.Version; + if (_completedPacketsCount > 0) + { + ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall); + ClearPreviousOperation(false); + if (bufferTooSmall) + { + return new ValueTask(Task.FromException(ThrowHelper.NewBufferTooSmallForBufferArgument())); + } + else + { + return new ValueTask(result); + } + } + + _activeWait = true; + Debug.Assert(!_signaled); + _cancellationToken = cancellationToken; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException())); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentReceiveException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _operationMode = 0; + _buffer = buffer; + + token = _mrvtsc.Version; + if (_completedPacketsCount > 0) + { + ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall); + ClearPreviousOperation(false); + if (bufferTooSmall) + { + return new ValueTask(Task.FromException(ThrowHelper.NewBufferTooSmallForBufferArgument())); + } + else + { + return new ValueTask(result.BytesReceived); + } + } + + _activeWait = true; + Debug.Assert(!_signaled); + _cancellationToken = cancellationToken; + } + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpReceiveQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken) + { + lock (_queue) + { + if (_activeWait && !_signaled) + { + ClearPreviousOperation(true); + _mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingReceive(innerException, cancellationToken)); + return true; + } + } + return false; + } + + private void SetCanceled() + { + lock (_queue) + { + if (_activeWait && !_signaled) + { + CancellationToken cancellationToken = _cancellationToken; + ClearPreviousOperation(true); + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + private void ClearPreviousOperation(bool signaled) + { + _signaled = signaled; + _operationMode = 0; + _buffer = default; + _minimumBytes = default; + _minimumSegments = default; + _cancellationToken = default; + } + + public void Enqueue(KcpBuffer buffer, byte fragment) + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + + if (_stream) + { + if (buffer.Length == 0) + { + return; + } + fragment = 0; + _queue.AddLast(_cache.Rent(buffer, 0)); + } + else + { + LinkedListNodeOfQueueItem? lastNode = _queue.Last; + if (lastNode is null || lastNode.ValueRef.Fragment == 0 || lastNode.ValueRef.Fragment - 1 == fragment) + { + _queue.AddLast(_cache.Rent(buffer, fragment)); + } + else + { + fragment = 0; + _queue.AddLast(_cache.Rent(buffer, 0)); + } + } + + if (fragment == 0) + { + _completedPacketsCount++; + if (_activeWait && !_signaled) + { + TryCompleteReceive(); + TryCompleteWaitForData(); + } + } + } + } + + private void TryCompleteReceive() + { + Debug.Assert(_activeWait && !_signaled); + + if (_operationMode <= 1) + { + Debug.Assert(_operationMode == 0 || _operationMode == 1); + ConsumePacket(_buffer.Span, out KcpConversationReceiveResult result, out bool bufferTooSmall); + ClearPreviousOperation(true); + if (bufferTooSmall) + { + _mrvtsc.SetException(ThrowHelper.NewBufferTooSmallForBufferArgument()); + } + else + { + _mrvtsc.SetResult(result); + } + } + } + + private void TryCompleteWaitForData() + { + if (_operationMode == 2) + { + if (CheckQueeuSize(_queue, _minimumBytes, _minimumSegments, _stream)) + { + ClearPreviousOperation(true); + _mrvtsc.SetResult(new KcpConversationReceiveResult(0)); + } + } + } + + private void ConsumePacket(Span buffer, out KcpConversationReceiveResult result, out bool bufferTooSmall) + { + LinkedListNodeOfQueueItem? node = _queue.First; + if (node is null) + { + result = default; + bufferTooSmall = false; + return; + } + + // peek + if (_operationMode == 1) + { + if (CalculatePacketSize(node, out int bytesRecevied)) + { + result = new KcpConversationReceiveResult(bytesRecevied); + } + else + { + result = default; + } + bufferTooSmall = false; + return; + } + + Debug.Assert(_operationMode == 0); + + // ensure buffer is big enough + int bytesInPacket = 0; + if (!_stream) + { + while (node is not null) + { + bytesInPacket += node.ValueRef.Data.Length; + if (node.ValueRef.Fragment == 0) + { + break; + } + node = node.Next; + } + + if (node is null) + { + // incomplete packet + result = default; + bufferTooSmall = false; + return; + } + + if (bytesInPacket > buffer.Length) + { + result = default; + bufferTooSmall = true; + return; + } + } + + bool anyDataReceived = false; + bytesInPacket = 0; + node = _queue.First; + LinkedListNodeOfQueueItem? next; + while (node is not null) + { + next = node.Next; + + byte fragment = node.ValueRef.Fragment; + ref KcpBuffer data = ref node.ValueRef.Data; + + int sizeToCopy = Math.Min(data.Length, buffer.Length); + data.DataRegion.Span.Slice(0, sizeToCopy).CopyTo(buffer); + buffer = buffer.Slice(sizeToCopy); + bytesInPacket += sizeToCopy; + anyDataReceived = true; + + if (sizeToCopy != data.Length) + { + // partial data is received. + node.ValueRef = (data.Consume(sizeToCopy), node.ValueRef.Fragment); + } + else + { + // full fragment is consumed + data.Release(); + _queue.Remove(node); + _cache.Return(node); + if (fragment == 0) + { + _completedPacketsCount--; + } + } + + if (!_stream && fragment == 0) + { + break; + } + + if (sizeToCopy == 0) + { + break; + } + + node = next; + } + + if (!anyDataReceived) + { + result = default; + bufferTooSmall = false; + } + else + { + result = new KcpConversationReceiveResult(bytesInPacket); + bufferTooSmall = false; + } + } + + private static bool CalculatePacketSize(LinkedListNodeOfQueueItem first, out int packetSize) + { + int bytesRecevied = first.ValueRef.Data.Length; + if (first.ValueRef.Fragment == 0) + { + packetSize = bytesRecevied; + return true; + } + + LinkedListNodeOfQueueItem? node = first.Next; + while (node is not null) + { + bytesRecevied += node.ValueRef.Data.Length; + if (node.ValueRef.Fragment == 0) + { + packetSize = bytesRecevied; + return true; + } + node = node.Next; + } + + // deadlink + packetSize = 0; + return false; + } + + private static bool CheckQueeuSize(LinkedListOfQueueItem queue, int minimumBytes, int minimumSegments, bool stream) + { + LinkedListNodeOfQueueItem? node = queue.First; + while (node is not null) + { + ref KcpBuffer buffer = ref node.ValueRef.Data; + minimumBytes = Math.Max(minimumBytes - buffer.Length, 0); + if (stream || node.ValueRef.Fragment == 0) + { + minimumSegments = Math.Max(minimumSegments - 1, 0); + } + if (minimumBytes == 0 && minimumSegments == 0) + { + return true; + } + node = node.Next; + } + + return minimumBytes == 0 && minimumSegments == 0; + } + + public void SetTransportClosed() + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(true); + _mrvtsc.SetResult(default); + } + _transportClosed = true; + } + } + + public int GetQueueSize() + { + int count; + lock (_queue) + { + count = _queue.Count; + } + return Math.Max(_queue.Count - _queueSize, 0); + } + + public void Dispose() + { + lock (_queue) + { + if (_disposed) + { + return; + } + if (_activeWait && !_signaled) + { + ClearPreviousOperation(true); + _mrvtsc.SetResult(default); + } + LinkedListNodeOfQueueItem? node = _queue.First; + while (node is not null) + { + node.ValueRef.Data.Release(); + node = node.Next; + } + _queue.Clear(); + _disposed = true; + _transportClosed = true; + } + } + } +} diff --git a/GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs b/GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs new file mode 100644 index 00000000..b7f52e39 --- /dev/null +++ b/GameServer/KcpSharp/KcpReceiveWindowNotificationOptions.cs @@ -0,0 +1,37 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Options for sending receive window size notification. + /// + public sealed class KcpReceiveWindowNotificationOptions + { + /// + /// Create an instance of option object for receive window size notification functionality. + /// + /// The initial interval in milliseconds of sending window size notification. + /// The maximum interval in milliseconds of sending window size notification. + public KcpReceiveWindowNotificationOptions(int initialInterval, int maximumInterval) + { + if (initialInterval <= 0) + { + throw new ArgumentOutOfRangeException(nameof(initialInterval)); + } + if (maximumInterval < initialInterval) + { + throw new ArgumentOutOfRangeException(nameof(maximumInterval)); + } + InitialInterval = initialInterval; + MaximumInterval = maximumInterval; + } + + /// + /// The initial interval in milliseconds of sending window size notification. + /// + public int InitialInterval { get; } + + /// + /// The maximum interval in milliseconds of sending window size notification. + /// + public int MaximumInterval { get; } + } +} diff --git a/GameServer/KcpSharp/KcpRentedBuffer.cs b/GameServer/KcpSharp/KcpRentedBuffer.cs new file mode 100644 index 00000000..9a831b72 --- /dev/null +++ b/GameServer/KcpSharp/KcpRentedBuffer.cs @@ -0,0 +1,222 @@ +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// The buffer rented and owned by KcpSharp. + /// + public readonly struct KcpRentedBuffer : IEquatable, IDisposable + { + private readonly object? _owner; + private readonly Memory _memory; + + internal object? Owner => _owner; + + /// + /// The rented buffer. + /// + public Memory Memory => _memory; + + /// + /// The rented buffer. + /// + public Span Span => _memory.Span; + + /// + /// Whether this struct contains buffer rented from the pool. + /// + public bool IsAllocated => _owner is not null; + + /// + /// Whether this buffer contains no data. + /// + public bool IsEmpry => _memory.IsEmpty; + + internal KcpRentedBuffer(object? owner, Memory buffer) + { + _owner = owner; + _memory = buffer; + } + + /// + /// Create the buffer from the specified . + /// + /// The memory region of this buffer. + /// The rented buffer. + public static KcpRentedBuffer FromMemory(Memory memory) + { + return new KcpRentedBuffer(null, memory); + } + + /// + /// Create the buffer from the shared array pool. + /// + /// The minimum size of the buffer required. + /// The rented buffer. + public static KcpRentedBuffer FromSharedArrayPool(int size) + { + if (size < 0) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + byte[] buffer = ArrayPool.Shared.Rent(size); + return new KcpRentedBuffer(ArrayPool.Shared, buffer); + } + + /// + /// Create the buffer from the specified array pool. + /// + /// The array pool to use. + /// The byte array rented from the specified pool. + /// The rented buffer. + public static KcpRentedBuffer FromArrayPool(ArrayPool pool, byte[] buffer) + { + if (pool is null) + { + throw new ArgumentNullException(nameof(pool)); + } + if (buffer is null) + { + throw new ArgumentNullException(nameof(buffer)); + } + return new KcpRentedBuffer(pool, buffer); + } + + /// + /// Create the buffer from the specified array pool. + /// + /// The array pool to use. + /// The byte array segment rented from the specified pool. + /// The rented buffer. + public static KcpRentedBuffer FromArrayPool(ArrayPool pool, ArraySegment arraySegment) + { + if (pool is null) + { + throw new ArgumentNullException(nameof(pool)); + } + return new KcpRentedBuffer(pool, arraySegment); + } + + /// + /// Create the buffer from the specified array pool. + /// + /// The array pool to use. + /// The minimum size of the buffer required. + /// The rented buffer. + public static KcpRentedBuffer FromArrayPool(ArrayPool pool, int size) + { + if (pool is null) + { + throw new ArgumentNullException(nameof(pool)); + } + if (size < 0) + { + throw new ArgumentOutOfRangeException(nameof(size)); + } + return new KcpRentedBuffer(pool, pool.Rent(size)); + } + + /// + /// Create the buffer from the memory owner. + /// + /// The owner of this memory region. + /// The rented buffer. + public static KcpRentedBuffer FromMemoryOwner(IMemoryOwner memoryOwner) + { + if (memoryOwner is null) + { + throw new ArgumentNullException(nameof(memoryOwner)); + } + return new KcpRentedBuffer(memoryOwner, memoryOwner.Memory); + } + + + /// + /// Create the buffer from the memory owner. + /// + /// The owner of this memory region. + /// The memory region of the buffer. + /// The rented buffer. + public static KcpRentedBuffer FromMemoryOwner(IDisposable memoryOwner, Memory memory) + { + if (memoryOwner is null) + { + throw new ArgumentNullException(nameof(memoryOwner)); + } + return new KcpRentedBuffer(memoryOwner, memory); + } + + /// + /// Forms a slice out of the current buffer that begins at a specified index. + /// + /// The index at which to begin the slice. + /// An object that contains all elements of the current instance from start to the end of the instance. + public KcpRentedBuffer Slice(int start) + { + Memory memory = _memory; + if ((uint)start > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start)); + } + return new KcpRentedBuffer(_owner, memory.Slice(start)); + } + + /// + /// Forms a slice out of the current memory starting at a specified index for a specified length. + /// + /// The index at which to begin the slice. + /// The number of elements to include in the slice. + /// An object that contains elements from the current instance starting at . + public KcpRentedBuffer Slice(int start, int length) + { + Memory memory = _memory; + if ((uint)start > (uint)memory.Length) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(start)); + } + if ((uint)length > (uint)(memory.Length - start)) + { + ThrowHelper.ThrowArgumentOutOfRangeException(nameof(length)); + } + return new KcpRentedBuffer(_owner, memory.Slice(start, length)); + } + + /// + public void Dispose() + { + Debug.Assert(_owner is null || _owner is ArrayPool || _owner is IDisposable); + + if (_owner is null) + { + return; + } + if (_owner is ArrayPool arrayPool) + { + if (MemoryMarshal.TryGetArray(_memory, out ArraySegment arraySegment)) + { + arrayPool.Return(arraySegment.Array!); + return; + } + } + if (_owner is IDisposable disposable) + { + disposable.Dispose(); + } + } + + /// + public bool Equals(KcpRentedBuffer other) => ReferenceEquals(_owner, other._owner) && _memory.Equals(other._memory); + + /// + public override bool Equals(object? obj) => obj is KcpRentedBuffer other && Equals(other); + + /// + public override int GetHashCode() => _owner is null ? _memory.GetHashCode() : HashCode.Combine(RuntimeHelpers.GetHashCode(_owner), _memory); + + /// + public override string ToString() => $"KcpSharp.KcpRentedBuffer[{_memory.Length}]"; + } +} diff --git a/GameServer/KcpSharp/KcpSendQueue.cs b/GameServer/KcpSharp/KcpSendQueue.cs new file mode 100644 index 00000000..2a2a6868 --- /dev/null +++ b/GameServer/KcpSharp/KcpSendQueue.cs @@ -0,0 +1,718 @@ +using System.Diagnostics; +using System.Threading.Tasks.Sources; + + + + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>; +#else +using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +#endif + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpSendQueue : IValueTaskSource, IValueTaskSource, IDisposable + { + private readonly IKcpBufferPool _bufferPool; + private readonly KcpConversationUpdateActivation _updateActivation; + private readonly bool _stream; + private readonly int _capacity; + private readonly int _mss; + private readonly KcpSendReceiveQueueItemCache _cache; + private ManualResetValueTaskSourceCore _mrvtsc; + + private readonly LinkedListOfQueueItem _queue; + private long _unflushedBytes; + + private bool _transportClosed; + private bool _disposed; + + private bool _activeWait; + private bool _signled; + private bool _forStream; + private byte _operationMode; // 0-send 1-flush 2-wait for space + private ReadOnlyMemory _buffer; + private int _waitForByteCount; + private int _waitForSegmentCount; + private CancellationToken _cancellationToken; + private CancellationTokenRegistration _cancellationRegistration; + + private bool _ackListNotEmpty; + public KcpSendQueue(IKcpBufferPool bufferPool, KcpConversationUpdateActivation updateActivation, bool stream, int capacity, int mss, KcpSendReceiveQueueItemCache cache) + { + _bufferPool = bufferPool; + _updateActivation = updateActivation; + _stream = stream; + _capacity = capacity; + _mss = mss; + _cache = cache; + _mrvtsc = new ManualResetValueTaskSourceCore() + { + RunContinuationsAsynchronously = true + }; + + _queue = new LinkedListOfQueueItem(); + } + + public ValueTaskSourceStatus GetStatus(short token) => _mrvtsc.GetStatus(token); + public void OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => _mrvtsc.OnCompleted(continuation, state, token, flags); + + bool IValueTaskSource.GetResult(short token) + { + _cancellationRegistration.Dispose(); + try + { + return _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signled = false; + _cancellationRegistration = default; + } + } + } + + void IValueTaskSource.GetResult(short token) + { + try + { + _mrvtsc.GetResult(token); + } + finally + { + _mrvtsc.Reset(); + lock (_queue) + { + _activeWait = false; + _signled = false; + _cancellationRegistration = default; + } + } + } + + public bool TryGetAvailableSpace(out int byteCount, out int segmentCount) + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + byteCount = 0; + segmentCount = 0; + return false; + } + if (_activeWait && _operationMode == 0) + { + byteCount = 0; + segmentCount = 0; + return true; + } + GetAvailableSpaceCore(out byteCount, out segmentCount); + return true; + } + } + + private void GetAvailableSpaceCore(out int byteCount, out int segmentCount) + { + int mss = _mss; + int availableFragments = _capacity - _queue.Count; + if (availableFragments < 0) + { + byteCount = 0; + segmentCount = 0; + return; + } + int availableBytes = availableFragments * mss; + if (_stream) + { + LinkedListNodeOfQueueItem? last = _queue.Last; + if (last is not null) + { + availableBytes += _mss - last.ValueRef.Data.Length; + } + } + byteCount = availableBytes; + segmentCount = availableFragments; + } + + public ValueTask WaitForAvailableSpaceAsync(int minimumBytes, int minimumSegments, CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + minimumBytes = 0; + minimumSegments = 0; + return default; + } + if ((uint)minimumBytes > (uint)(_mss * _capacity)) + { + return new ValueTask(Task.FromException(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumBytes)))); + } + if ((uint)minimumSegments > (uint)_capacity) + { + return new ValueTask(Task.FromException(ThrowHelper.NewArgumentOutOfRangeException(nameof(minimumSegments)))); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + GetAvailableSpaceCore(out int currentByteCount, out int currentSegmentCount); + if (currentByteCount >= minimumBytes && currentSegmentCount >= minimumSegments) + { + return new ValueTask(true); + } + + _activeWait = true; + Debug.Assert(!_signled); + _forStream = false; + _operationMode = 2; + _waitForByteCount = minimumBytes; + _waitForSegmentCount = minimumSegments; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool TrySend(ReadOnlySpan buffer, bool allowPartialSend, out int bytesWritten) + { + lock (_queue) + { + if (allowPartialSend && !_stream) + { + ThrowHelper.ThrowAllowPartialSendArgumentException(); + } + if (_transportClosed || _disposed) + { + bytesWritten = 0; + return false; + } + + int mss = _mss; + // Make sure there is enough space. + if (!allowPartialSend) + { + int spaceAvailable = mss * (_capacity - _queue.Count); + if (spaceAvailable < 0) + { + bytesWritten = 0; + return false; + } + if (_stream) + { + LinkedListNodeOfQueueItem? last = _queue.Last; + if (last is not null) + { + spaceAvailable += mss - last.ValueRef.Data.Length; + } + } + + if (buffer.Length > spaceAvailable) + { + bytesWritten = 0; + return false; + } + } + + // Copy buffer content. + bytesWritten = 0; + if (_stream) + { + LinkedListNodeOfQueueItem? node = _queue.Last; + if (node is not null) + { + ref KcpBuffer data = ref node.ValueRef.Data; + int expand = mss - data.Length; + expand = Math.Min(expand, buffer.Length); + if (expand > 0) + { + data = data.AppendData(buffer.Slice(0, expand)); + buffer = buffer.Slice(expand); + Interlocked.Add(ref _unflushedBytes, expand); + bytesWritten = expand; + } + } + + if (buffer.IsEmpty) + { + return true; + } + } + + bool anySegmentAdded = false; + int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss; + Debug.Assert(count >= 1); + while (count > 0 && _queue.Count < _capacity) + { + int fragment = --count; + + int size = buffer.Length > mss ? mss : buffer.Length; + + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false)); + KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Slice(0, size)); + buffer = buffer.Slice(size); + + _queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment)); + Interlocked.Add(ref _unflushedBytes, size); + bytesWritten += size; + anySegmentAdded = true; + } + + if (anySegmentAdded) + { + _updateActivation.Notify(); + } + return anySegmentAdded; + } + } + + public ValueTask SendAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return new ValueTask(false); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + int mss = _mss; + if (_stream) + { + LinkedListNodeOfQueueItem? node = _queue.Last; + if (node is not null) + { + ref KcpBuffer data = ref node.ValueRef.Data; + int expand = mss - data.Length; + expand = Math.Min(expand, buffer.Length); + if (expand > 0) + { + data = data.AppendData(buffer.Span.Slice(0, expand)); + buffer = buffer.Slice(expand); + Interlocked.Add(ref _unflushedBytes, expand); + } + } + + if (buffer.IsEmpty) + { + return new ValueTask(true); + } + } + + int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss; + Debug.Assert(count >= 1); + + if (!_stream && count > 256) + { + return new ValueTask(Task.FromException(ThrowHelper.NewMessageTooLargeForBufferArgument())); + } + + // synchronously put fragments into queue. + while (count > 0 && _queue.Count < _capacity) + { + int fragment = --count; + + int size = buffer.Length > mss ? mss : buffer.Length; + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false)); + KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size)); + buffer = buffer.Slice(size); + + _queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)fragment)); + Interlocked.Add(ref _unflushedBytes, size); + } + + _updateActivation.Notify(); + + if (count == 0) + { + return new ValueTask(true); + } + + _activeWait = true; + Debug.Assert(!_signled); + _forStream = false; + _operationMode = 0; + _buffer = buffer; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException())); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + int mss = _mss; + if (_stream) + { + LinkedListNodeOfQueueItem? node = _queue.Last; + if (node is not null) + { + ref KcpBuffer data = ref node.ValueRef.Data; + int expand = mss - data.Length; + expand = Math.Min(expand, buffer.Length); + if (expand > 0) + { + data = data.AppendData(buffer.Span.Slice(0, expand)); + buffer = buffer.Slice(expand); + Interlocked.Add(ref _unflushedBytes, expand); + } + } + + if (buffer.IsEmpty) + { + return default; + } + } + + int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss; + Debug.Assert(count >= 1); + + Debug.Assert(_stream); + // synchronously put fragments into queue. + while (count > 0 && _queue.Count < _capacity) + { + int size = buffer.Length > mss ? mss : buffer.Length; + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false)); + KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size)); + buffer = buffer.Slice(size); + + _queue.AddLast(_cache.Rent(kcpBuffer, 0)); + Interlocked.Add(ref _unflushedBytes, size); + } + + _updateActivation.Notify(); + + if (count == 0) + { + return default; + } + + _activeWait = true; + Debug.Assert(!_signled); + _forStream = true; + _operationMode = 0; + _buffer = buffer; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public ValueTask FlushAsync(CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return new ValueTask(false); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _activeWait = true; + Debug.Assert(!_signled); + _forStream = false; + _operationMode = 1; + _buffer = default; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public ValueTask FlushForStreamAsync(CancellationToken cancellationToken) + { + short token; + lock (_queue) + { + if (_transportClosed || _disposed) + { + return new ValueTask(Task.FromException(ThrowHelper.NewTransportClosedForStreamException())); + } + if (_activeWait) + { + return new ValueTask(Task.FromException(ThrowHelper.NewConcurrentSendException())); + } + if (cancellationToken.IsCancellationRequested) + { + return new ValueTask(Task.FromCanceled(cancellationToken)); + } + + _activeWait = true; + Debug.Assert(!_signled); + _forStream = true; + _operationMode = 1; + _buffer = default; + _cancellationToken = cancellationToken; + token = _mrvtsc.Version; + } + + _cancellationRegistration = cancellationToken.UnsafeRegister(state => ((KcpSendQueue?)state)!.SetCanceled(), this); + + return new ValueTask(this, token); + } + + public bool CancelPendingOperation(Exception? innerException, CancellationToken cancellationToken) + { + lock (_queue) + { + if (_activeWait && !_signled) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewOperationCanceledExceptionForCancelPendingSend(innerException, cancellationToken)); + return true; + } + } + return false; + } + + private void SetCanceled() + { + lock (_queue) + { + if (_activeWait && !_signled) + { + CancellationToken cancellationToken = _cancellationToken; + ClearPreviousOperation(); + _mrvtsc.SetException(new OperationCanceledException(cancellationToken)); + } + } + } + + private void ClearPreviousOperation() + { + _signled = true; + _forStream = false; + _operationMode = 0; + _buffer = default; + _waitForByteCount = default; + _waitForSegmentCount = default; + _cancellationToken = default; + } + + public bool TryDequeue(out KcpBuffer data, out byte fragment) + { + lock (_queue) + { + LinkedListNodeOfQueueItem? node = _queue.First; + if (node is null) + { + data = default; + fragment = default; + return false; + } + else + { + (data, fragment) = node.ValueRef; + _queue.RemoveFirst(); + node.ValueRef = default; + _cache.Return(node); + + MoveOneSegmentIn(); + CheckForAvailableSpace(); + return true; + } + } + } + + public void NotifyAckListChanged(bool itemsListNotEmpty) + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + + _ackListNotEmpty = itemsListNotEmpty; + TryCompleteFlush(Interlocked.Read(ref _unflushedBytes)); + } + } + + private void MoveOneSegmentIn() + { + if (_activeWait && !_signled && _operationMode == 0) + { + ReadOnlyMemory buffer = _buffer; + int mss = _mss; + int count = buffer.Length <= mss ? 1 : (buffer.Length + mss - 1) / mss; + + int size = buffer.Length > mss ? mss : buffer.Length; + KcpRentedBuffer owner = _bufferPool.Rent(new KcpBufferPoolRentOptions(mss, false)); + KcpBuffer kcpBuffer = KcpBuffer.CreateFromSpan(owner, buffer.Span.Slice(0, size)); + _buffer = buffer.Slice(size); + + _queue.AddLast(_cache.Rent(kcpBuffer, _stream ? (byte)0 : (byte)(count - 1))); + Interlocked.Add(ref _unflushedBytes, size); + + if (count == 1) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(true); + } + } + } + + private void CheckForAvailableSpace() + { + if (_activeWait && !_signled && _operationMode == 2) + { + GetAvailableSpaceCore(out int byteCount, out int segmentCount); + if (byteCount >= _waitForByteCount && segmentCount >= _waitForSegmentCount) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(true); + } + } + } + + private void TryCompleteFlush(long unflushedBytes) + { + if (_activeWait && !_signled && _operationMode == 1) + { + if (_queue.Last is null && unflushedBytes == 0 && !_ackListNotEmpty) + { + ClearPreviousOperation(); + _mrvtsc.SetResult(true); + } + } + } + + public void SubtractUnflushedBytes(int size) + { + long unflushedBytes = Interlocked.Add(ref _unflushedBytes, -size); + if (unflushedBytes == 0) + { + lock (_queue) + { + TryCompleteFlush(0); + } + } + } + + public long GetUnflushedBytes() + { + if (_transportClosed || _disposed) + { + return 0; + } + return Interlocked.Read(ref _unflushedBytes); + } + + public void SetTransportClosed() + { + lock (_queue) + { + if (_transportClosed || _disposed) + { + return; + } + if (_activeWait && !_signled) + { + if (_forStream) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException()); + } + else + { + ClearPreviousOperation(); + _mrvtsc.SetResult(false); + } + } + _transportClosed = true; + Interlocked.Exchange(ref _unflushedBytes, 0); + } + } + + public void Dispose() + { + lock (_queue) + { + if (_disposed) + { + return; + } + if (_activeWait && !_signled) + { + if (_forStream) + { + ClearPreviousOperation(); + _mrvtsc.SetException(ThrowHelper.NewTransportClosedForStreamException()); + } + else + { + ClearPreviousOperation(); + _mrvtsc.SetResult(false); + } + } + LinkedListNodeOfQueueItem? node = _queue.First; + while (node is not null) + { + node.ValueRef.Data.Release(); + node = node.Next; + } + _queue.Clear(); + _disposed = true; + _transportClosed = true; + } + } + + } +} diff --git a/GameServer/KcpSharp/KcpSendReceiveBufferItem.cs b/GameServer/KcpSharp/KcpSendReceiveBufferItem.cs new file mode 100644 index 00000000..9c1ab0f4 --- /dev/null +++ b/GameServer/KcpSharp/KcpSendReceiveBufferItem.cs @@ -0,0 +1,9 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal struct KcpSendReceiveBufferItem + { + public KcpBuffer Data; + public KcpPacketHeader Segment; + public KcpSendSegmentStats Stats; + } +} diff --git a/GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs b/GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs new file mode 100644 index 00000000..f0e83d8a --- /dev/null +++ b/GameServer/KcpSharp/KcpSendReceiveBufferItemCache.cs @@ -0,0 +1,73 @@ + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfBufferItem = KcpSharp.NetstandardShim.LinkedList; +using LinkedListNodeOfBufferItem = KcpSharp.NetstandardShim.LinkedListNode; +#else +using LinkedListNodeOfBufferItem = System.Collections.Generic.LinkedListNode; +using LinkedListOfBufferItem = System.Collections.Generic.LinkedList; +#endif + +namespace EggLink.DanhengServer.KcpSharp +{ + internal struct KcpSendReceiveBufferItemCache + { + private LinkedListOfBufferItem _items; + private SpinLock _lock; + + public static KcpSendReceiveBufferItemCache Create() + { + return new KcpSendReceiveBufferItemCache + { + _items = new LinkedListOfBufferItem(), + _lock = new SpinLock() + }; + } + + public LinkedListNodeOfBufferItem Allocate(in KcpSendReceiveBufferItem item) + { + bool lockAcquired = false; + try + { + _lock.Enter(ref lockAcquired); + + LinkedListNodeOfBufferItem? node = _items.First; + if (node is null) + { + node = new LinkedListNodeOfBufferItem(item); + } + else + { + _items.Remove(node); + node.ValueRef = item; + } + return node; + } + finally + { + if (lockAcquired) + { + _lock.Exit(); + } + } + } + + public void Return(LinkedListNodeOfBufferItem node) + { + bool lockAcquired = false; + try + { + _lock.Enter(ref lockAcquired); + + node.ValueRef = default; + _items.AddLast(node); + } + finally + { + if (lockAcquired) + { + _lock.Exit(); + } + } + } + } +} diff --git a/GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs b/GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs new file mode 100644 index 00000000..9b5be70f --- /dev/null +++ b/GameServer/KcpSharp/KcpSendReceiveQueueItemCache.cs @@ -0,0 +1,84 @@ + +#if NEED_LINKEDLIST_SHIM +using LinkedListOfQueueItem = KcpSharp.NetstandardShim.LinkedList<(KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListNodeOfQueueItem = KcpSharp.NetstandardShim.LinkedListNode<(KcpSharp.KcpBuffer Data, byte Fragment)>; +#else +using LinkedListNodeOfQueueItem = System.Collections.Generic.LinkedListNode<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +using LinkedListOfQueueItem = System.Collections.Generic.LinkedList<(EggLink.DanhengServer.KcpSharp.KcpBuffer Data, byte Fragment)>; +#endif + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpSendReceiveQueueItemCache + { + private LinkedListOfQueueItem _list = new(); + private SpinLock _lock; + + public LinkedListNodeOfQueueItem Rent(in KcpBuffer buffer, byte fragment) + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + LinkedListNodeOfQueueItem? node = _list.First; + if (node is null) + { + node = new LinkedListNodeOfQueueItem((buffer, fragment)); + } + else + { + node.ValueRef = (buffer, fragment); + _list.RemoveFirst(); + } + + return node; + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + + public void Return(LinkedListNodeOfQueueItem node) + { + node.ValueRef = default; + + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + _list.AddLast(node); + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + + public void Clear() + { + bool lockTaken = false; + try + { + _lock.Enter(ref lockTaken); + + _list.Clear(); + } + finally + { + if (lockTaken) + { + _lock.Exit(); + } + } + } + } +} diff --git a/GameServer/KcpSharp/KcpSendSegmentStats.cs b/GameServer/KcpSharp/KcpSendSegmentStats.cs new file mode 100644 index 00000000..2cec9155 --- /dev/null +++ b/GameServer/KcpSharp/KcpSendSegmentStats.cs @@ -0,0 +1,20 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal readonly struct KcpSendSegmentStats + { + public KcpSendSegmentStats(uint resendTimestamp, uint rto, uint fastAck, uint transmitCount) + { + ResendTimestamp = resendTimestamp; + Rto = rto; + FastAck = fastAck; + TransmitCount = transmitCount; + } + + public uint ResendTimestamp { get; } + public uint Rto { get; } + public uint FastAck { get; } + public uint TransmitCount { get; } + + + } +} diff --git a/GameServer/KcpSharp/KcpSocketTransport.cs b/GameServer/KcpSharp/KcpSocketTransport.cs new file mode 100644 index 00000000..3e5a0fdf --- /dev/null +++ b/GameServer/KcpSharp/KcpSocketTransport.cs @@ -0,0 +1,159 @@ +using KcpSharp; +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + + /// + /// Helper methods to create socket transports for KCP conversations. + /// + public static class KcpSocketTransport + { + /// + /// Create a socket transport for KCP covnersation. + /// + /// The udp listener instance. + /// The remote endpoint. + /// The conversation ID. + /// The options of the . + /// The created socket transport instance. + public static IKcpTransport CreateConversation(UdpClient listener, IPEndPoint endPoint, long conversationId, KcpConversationOptions? options) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForConversation(listener, endPoint, conversationId, options); + } + + /// + /// Create a socket transport for KCP covnersation with no conversation ID. + /// + /// The udp listener instance. + /// The remote endpoint. + /// The options of the . + /// The created socket transport instance. + public static IKcpTransport CreateConversation(UdpClient listener, IPEndPoint endPoint, KcpConversationOptions? options) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForConversation(listener, endPoint, null, options); + } + + /// + /// Create a socket transport for raw channel. + /// + /// The udp listener instance. + /// The remote endpoint. + /// The conversation ID. + /// The options of the . + /// The created socket transport instance. + public static IKcpTransport CreateRawChannel(UdpClient listener, IPEndPoint endPoint, long conversationId, KcpRawChannelOptions? options) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForRawChannel(listener, endPoint, conversationId, options); + } + + /// + /// Create a socket transport for raw channel with no conversation ID. + /// + /// The udp listener instance. + /// The remote endpoint. + /// The options of the . + /// The created socket transport instance. + public static IKcpTransport CreateRawChannel(UdpClient listener, IPEndPoint endPoint, KcpRawChannelOptions? options) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForRawChannel(listener, endPoint, null, options); + } + + /// + /// Create a socket transport for multiplex connection. + /// + /// The udp listener instance. + /// The maximum packet size that can be transmitted over the socket. + /// + public static IKcpTransport CreateMultiplexConnection(UdpClient listener, int mtu) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + + return new KcpSocketTransportForMultiplexConnection(listener, mtu); + } + + /// + /// Create a socket transport for multiplex connection. + /// + /// The type of the user state. + /// The udp listener instance. + /// The maximum packet size that can be transmitted over the socket. + /// + public static IKcpTransport> CreateMultiplexConnection(UdpClient listener, IPEndPoint endPoint, int mtu) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForMultiplexConnection(listener, mtu); + } + + /// + /// Create a socket transport for multiplex connection. + /// + /// The type of the user state. + /// The udp listener instance. + /// The remote endpoint. + /// The maximum packet size that can be transmitted over the socket. + /// The action to invoke when state object is removed. + /// + public static IKcpTransport> CreateMultiplexConnection(UdpClient listener, EndPoint endPoint, int mtu, Action? disposeAction) + { + if (listener is null) + { + throw new ArgumentNullException(nameof(listener)); + } + if (endPoint is null) + { + throw new ArgumentNullException(nameof(endPoint)); + } + + return new KcpSocketTransportForMultiplexConnection(listener, mtu, disposeAction); + } + } +} diff --git a/GameServer/KcpSharp/KcpSocketTransportForConversation.cs b/GameServer/KcpSharp/KcpSocketTransportForConversation.cs new file mode 100644 index 00000000..0406ed8e --- /dev/null +++ b/GameServer/KcpSharp/KcpSocketTransportForConversation.cs @@ -0,0 +1,46 @@ +using KcpSharp; +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// Socket transport for KCP conversation. + /// + internal sealed class KcpSocketTransportForConversation : KcpSocketTransport, IKcpTransport + { + private readonly long? _conversationId; + private readonly IPEndPoint _remoteEndPoint; + private readonly KcpConversationOptions? _options; + + private Func, object?, bool>? _exceptionHandler; + private object? _exceptionHandlerState; + + + internal KcpSocketTransportForConversation(UdpClient listener, IPEndPoint endPoint, long? conversationId, KcpConversationOptions? options) + : base(listener, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue) + { + _conversationId = conversationId; + _remoteEndPoint = endPoint; + _options = options; + } + + protected override KcpConversation Activate() => _conversationId.HasValue ? new KcpConversation(_remoteEndPoint, this, _conversationId.GetValueOrDefault(), _options) : new KcpConversation(_remoteEndPoint, this, _options); + + protected override bool HandleException(Exception ex) + { + if (_exceptionHandler is not null) + { + return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); + } + return false; + } + + public void SetExceptionHandler(Func, object?, bool> handler, object? state) + { + _exceptionHandler = handler; + _exceptionHandlerState = state; + } + + } +} diff --git a/GameServer/KcpSharp/KcpSocketTransportForMultiplexConnection.cs b/GameServer/KcpSharp/KcpSocketTransportForMultiplexConnection.cs new file mode 100644 index 00000000..5d04ba48 --- /dev/null +++ b/GameServer/KcpSharp/KcpSocketTransportForMultiplexConnection.cs @@ -0,0 +1,40 @@ +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpSocketTransportForMultiplexConnection : KcpSocketTransport>, IKcpTransport> + { + private readonly Action? _disposeAction; + private Func>, object?, bool>? _exceptionHandler; + private object? _exceptionHandlerState; + + internal KcpSocketTransportForMultiplexConnection(UdpClient listener, int mtu) + : base(listener, mtu) + { } + + internal KcpSocketTransportForMultiplexConnection(UdpClient listener, int mtu, Action? disposeAction) + : base(listener, mtu) + { + _disposeAction = disposeAction; + } + + protected override KcpMultiplexConnection Activate() => new(this, _disposeAction); + + IKcpMultiplexConnection IKcpTransport>.Connection => Connection; + + protected override bool HandleException(Exception ex) + { + if (_exceptionHandler is not null) + { + return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); + } + return false; + } + + public void SetExceptionHandler(Func>, object?, bool> handler, object? state) + { + _exceptionHandler = handler; + _exceptionHandlerState = state; + } + } +} diff --git a/GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs b/GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs new file mode 100644 index 00000000..d154c049 --- /dev/null +++ b/GameServer/KcpSharp/KcpSocketTransportForRawChannel.cs @@ -0,0 +1,41 @@ +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + internal sealed class KcpSocketTransportForRawChannel : KcpSocketTransport, IKcpTransport + { + private readonly long? _conversationId; + private readonly IPEndPoint _remoteEndPoint; + private readonly KcpRawChannelOptions? _options; + + private Func, object?, bool>? _exceptionHandler; + private object? _exceptionHandlerState; + + + internal KcpSocketTransportForRawChannel(UdpClient listener, IPEndPoint endPoint, long? conversationId, KcpRawChannelOptions? options) + : base(listener, options?.Mtu ?? KcpConversationOptions.MtuDefaultValue) + { + _conversationId = conversationId; + _remoteEndPoint = endPoint; + _options = options; + } + + protected override KcpRawChannel Activate() => _conversationId.HasValue ? new KcpRawChannel(_remoteEndPoint, this, _conversationId.GetValueOrDefault(), _options) : new KcpRawChannel(_remoteEndPoint, this, _options); + + protected override bool HandleException(Exception ex) + { + if (_exceptionHandler is not null) + { + return _exceptionHandler.Invoke(ex, this, _exceptionHandlerState); + } + return false; + } + + public void SetExceptionHandler(Func, object?, bool> handler, object? state) + { + _exceptionHandler = handler; + _exceptionHandlerState = state; + } + } +} diff --git a/GameServer/KcpSharp/KcpSocketTransportOfT.cs b/GameServer/KcpSharp/KcpSocketTransportOfT.cs new file mode 100644 index 00000000..a96d544e --- /dev/null +++ b/GameServer/KcpSharp/KcpSocketTransportOfT.cs @@ -0,0 +1,222 @@ +using EggLink.DanhengServer.Server; +using EggLink.DanhengServer.Util; +using System.Buffers; +using System.Net; +using System.Net.Sockets; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// A Socket transport for upper-level connections. + /// + /// + public abstract class KcpSocketTransport : IKcpTransport, IDisposable where T : class, IKcpConversation + { + private readonly UdpClient _udpListener; + private readonly int _mtu; + private T? _connection; + private CancellationTokenSource? _cts; + private bool _disposed; + + /// + /// Construct a socket transport with the specified socket and remote endpoint. + /// + /// The socket instance. + /// The maximum packet size that can be transmitted. + protected KcpSocketTransport(UdpClient listener, int mtu) + { + _udpListener = listener ?? throw new ArgumentNullException(nameof(listener)); + _mtu = mtu; + if (mtu < 50) + { + throw new ArgumentOutOfRangeException(nameof(mtu)); + } + } + + /// + /// Get the upper-level connection instace. If Start is not called or the transport is closed, will be thrown. + /// + /// Start is not called or the transport is closed. + public T Connection => _connection ?? throw new InvalidOperationException(); + + /// + /// Create the upper-level connection instance. + /// + /// The upper-level connection instance. + protected abstract T Activate(); + + /// + /// Allocate a block of memory used to receive from socket. + /// + /// The minimum size of the buffer. + /// The allocated memory buffer. + protected virtual IMemoryOwner AllocateBuffer(int size) + { +#if NEED_POH_SHIM + return MemoryPool.Shared.Rent(size); +#else + return new ArrayMemoryOwner(GC.AllocateUninitializedArray(size, pinned: true)); +#endif + } + + /// + /// Handle exception thrown when receiving from remote endpoint. + /// + /// The exception thrown. + /// Whether error should be ignored. + protected virtual bool HandleException(Exception ex) => false; + + /// + /// Create the upper-level connection and start pumping packets from the socket to the upper-level connection. + /// + public void Start() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(KcpSocketTransport)); + } + if (_connection is not null) + { + throw new InvalidOperationException(); + } + + _connection = Activate(); + if (_connection is null) + { + throw new InvalidOperationException(); + } + _cts = new CancellationTokenSource(); + RunReceiveLoop(); + } + + /// + public ValueTask SendPacketAsync(Memory packet, IPEndPoint endpoint, CancellationToken cancellationToken = default) + { + if (_disposed) + { + return default; + } + if (packet.Length > _mtu) + { + return default; + } + + return new ValueTask(_udpListener.SendAsync(packet.ToArray(), endpoint, cancellationToken).AsTask()); + } + + private async void RunReceiveLoop() + { + CancellationToken cancellationToken = _cts?.Token ?? new CancellationToken(true); + IKcpConversation? connection = _connection; + if (connection is null || cancellationToken.IsCancellationRequested) + { + return; + } + + using IMemoryOwner memoryOwner = AllocateBuffer(_mtu); + try + { + while (!cancellationToken.IsCancellationRequested) + { + int bytesReceived = 0; + bool error = false; + UdpReceiveResult result = default; + try + { + result = await _udpListener.ReceiveAsync(cancellationToken); + bytesReceived = result.Buffer.Length; + } + catch (Exception ex) + { + /*if (!HandleExceptionWrapper(ex)) + { + break; + } + error = true; + */ + } + + if (bytesReceived != 0 && bytesReceived <= _mtu) + { + if (bytesReceived == Listener.HANDSHAKE_SIZE) + await Listener.HandleHandshake(result); + else if (!error) + await connection.InputPakcetAsync(result, cancellationToken).ConfigureAwait(false); + } + } + } + catch (OperationCanceledException) + { + // Do nothing + } + catch (Exception ex) + { + HandleExceptionWrapper(ex); + } + } + + private bool HandleExceptionWrapper(Exception ex) + { + bool result; + try + { + new Logger("KcpServer").Error("KCP Error:", ex); + result = HandleException(ex); + } + catch + { + result = false; + } + + _connection?.SetTransportClosed(); + CancellationTokenSource? cts = Interlocked.Exchange(ref _cts, null); + if (cts is not null) + { + cts.Cancel(); + cts.Dispose(); + } + + return result; + } + + /// + /// Dispose all the managed and the unmanaged resources used by this instance. + /// + /// If managed resources should be disposed. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + CancellationTokenSource? cts = Interlocked.Exchange(ref _cts, null); + if (cts is not null) + { + cts.Cancel(); + cts.Dispose(); + } + _connection?.Dispose(); + } + + _connection = null; + _cts = null; + _disposed = true; + } + } + + /// + /// Dispose the unmanaged resources used by this instance. + /// + ~KcpSocketTransport() + { + Dispose(disposing: false); + } + + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/GameServer/KcpSharp/KcpStream.cs b/GameServer/KcpSharp/KcpStream.cs new file mode 100644 index 00000000..0bc6b8c6 --- /dev/null +++ b/GameServer/KcpSharp/KcpStream.cs @@ -0,0 +1,168 @@ +using KcpSharp; + +namespace EggLink.DanhengServer.KcpSharp +{ + /// + /// A stream wrapper of . + /// + public sealed class KcpStream : Stream + { + private KcpConversation? _conversation; + private readonly bool _ownsConversation; + + /// + /// Create a stream wrapper over an existing instance. + /// + /// The conversation instance. It must be in stream mode. + /// Whether to dispose the instance when is disposed. + public KcpStream(KcpConversation conversation, bool ownsConversation) + { + if (conversation is null) + { + throw new ArgumentNullException(nameof(conversation)); + } + if (!conversation.StreamMode) + { + throw new ArgumentException("Non-stream mode conversation is not supported.", nameof(conversation)); + } + _conversation = conversation; + _ownsConversation = ownsConversation; + } + + /// + public override bool CanRead => true; + + /// + public override bool CanSeek => false; + + /// + public override bool CanWrite => true; + + /// + /// The length of the stream. This always throws . + /// + public override long Length => throw new NotSupportedException(); + + /// + /// The position of the stream. This always throws . + /// + public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } + + /// + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + /// + public override void SetLength(long value) => throw new NotSupportedException(); + + /// + /// Indicates data is available on the stream to be read. This property checks to see if at least one byte of data is currently available + /// + public bool DataAvailable + { + get + { + if (_conversation is null) + { + ThrowHelper.ThrowObjectDisposedForKcpStreamException(); + } + return _conversation!.TryPeek(out KcpConversationReceiveResult result) && result.BytesReceived != 0; + } + } + + /// + public override void Flush() => throw new NotSupportedException(); + + /// + public override Task FlushAsync(CancellationToken cancellationToken) + { + if (_conversation is null) + { + return Task.FromException(ThrowHelper.NewObjectDisposedForKcpStreamException()); + } + return _conversation!.FlushAsync(cancellationToken).AsTask(); + } + + /// + public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + /// + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_conversation is null) + { + return Task.FromException(new ObjectDisposedException(nameof(KcpStream))); + } + return _conversation.ReadAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + /// + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (_conversation is null) + { + return Task.FromException(new ObjectDisposedException(nameof(KcpStream))); + } + return _conversation.WriteAsync(buffer.AsMemory(offset, count), cancellationToken).AsTask(); + } + + /// + public override int ReadByte() => throw new NotSupportedException(); + + /// + public override void WriteByte(byte value) => throw new NotSupportedException(); + + /// + protected override void Dispose(bool disposing) + { + if (disposing && _ownsConversation) + { + _conversation?.Dispose(); + } + _conversation = null; + base.Dispose(disposing); + } + +#if !NO_FAST_SPAN + /// + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + if (_conversation is null) + { + return new ValueTask(Task.FromException(new ObjectDisposedException(nameof(KcpStream)))); + } + return _conversation.ReadAsync(buffer, cancellationToken); + } + + /// + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + if (_conversation is null) + { + return new ValueTask(Task.FromException(new ObjectDisposedException(nameof(KcpStream)))); + } + return _conversation.WriteAsync(buffer, cancellationToken); + } + + /// + public override ValueTask DisposeAsync() + { + if (_conversation is not null) + { + _conversation.Dispose(); + _conversation = null; + } + return base.DisposeAsync(); + } + + /// + public override int Read(Span buffer) => throw new NotSupportedException(); + + /// + public override void Write(ReadOnlySpan buffer) => throw new NotSupportedException(); +#endif + + } +} diff --git a/GameServer/KcpSharp/NetstandardShim/AwaitableSocketAsyncEventArgs.cs b/GameServer/KcpSharp/NetstandardShim/AwaitableSocketAsyncEventArgs.cs new file mode 100644 index 00000000..a3392849 --- /dev/null +++ b/GameServer/KcpSharp/NetstandardShim/AwaitableSocketAsyncEventArgs.cs @@ -0,0 +1,36 @@ +#if NEED_SOCKET_SHIM + +using System; +using System.Net.Sockets; +using System.Threading.Tasks; +using System.Threading.Tasks.Sources; + +namespace KcpSharp +{ + internal class AwaitableSocketAsyncEventArgs : SocketAsyncEventArgs, IValueTaskSource + { + private ManualResetValueTaskSourceCore _mrvtsc = new ManualResetValueTaskSourceCore { RunContinuationsAsynchronously = true }; + + void IValueTaskSource.GetResult(short token) => _mrvtsc.GetResult(token); + ValueTaskSourceStatus IValueTaskSource.GetStatus(short token) => _mrvtsc.GetStatus(token); + void IValueTaskSource.OnCompleted(Action continuation, object? state, short token, ValueTaskSourceOnCompletedFlags flags) + => _mrvtsc.OnCompleted(continuation, state, token, flags); + + protected override void OnCompleted(SocketAsyncEventArgs e) + { + _mrvtsc.SetResult(true); + } + + public ValueTask WaitAsync() + { + return new ValueTask(this, _mrvtsc.Version); + } + + public void Reset() + { + _mrvtsc.Reset(); + } + } +} + +#endif diff --git a/GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs b/GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs new file mode 100644 index 00000000..31ea5037 --- /dev/null +++ b/GameServer/KcpSharp/NetstandardShim/CancellationTokenShim.cs @@ -0,0 +1,13 @@ +#if NEED_CANCELLATIONTOKEN_SHIM + +namespace System.Threading +{ + internal static class CancellationTokenShim + { + public static CancellationTokenRegistration UnsafeRegister(this CancellationToken cancellationToken, Action callback, object? state) + => cancellationToken.Register(callback, state); + } +} + + +#endif diff --git a/GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs b/GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs new file mode 100644 index 00000000..63b4658d --- /dev/null +++ b/GameServer/KcpSharp/NetstandardShim/LinkedListNetstandard.cs @@ -0,0 +1,213 @@ +#if NEED_LINKEDLIST_SHIM + +using System; +using System.Diagnostics; + +namespace KcpSharp.NetstandardShim +{ + internal class LinkedList + { + // This LinkedList is a doubly-Linked circular list. + internal LinkedListNode? head; + internal int count; + internal int version; + + public int Count + { + get { return count; } + } + + public LinkedListNode? First + { + get { return head; } + } + + public LinkedListNode? Last + { + get { return head == null ? null : head.prev; } + } + + public void AddAfter(LinkedListNode node, LinkedListNode newNode) + { + ValidateNode(node); + ValidateNewNode(newNode); + InternalInsertNodeBefore(node.next!, newNode); + newNode.list = this; + } + + public void AddBefore(LinkedListNode node, LinkedListNode newNode) + { + ValidateNode(node); + ValidateNewNode(newNode); + InternalInsertNodeBefore(node, newNode); + newNode.list = this; + if (node == head) + { + head = newNode; + } + } + + public void AddFirst(LinkedListNode node) + { + ValidateNewNode(node); + + if (head == null) + { + InternalInsertNodeToEmptyList(node); + } + else + { + InternalInsertNodeBefore(head, node); + head = node; + } + node.list = this; + } + + public void AddLast(LinkedListNode node) + { + ValidateNewNode(node); + + if (head == null) + { + InternalInsertNodeToEmptyList(node); + } + else + { + InternalInsertNodeBefore(head, node); + } + node.list = this; + } + + public void Clear() + { + LinkedListNode? current = head; + while (current != null) + { + LinkedListNode temp = current; + current = current.Next; // use Next the instead of "next", otherwise it will loop forever + temp.Invalidate(); + } + + head = null; + count = 0; + version++; + } + + public void Remove(LinkedListNode node) + { + ValidateNode(node); + InternalRemoveNode(node); + } + + public void RemoveFirst() + { + if (head == null) { throw new InvalidOperationException(); } + InternalRemoveNode(head); + } + + private void InternalInsertNodeBefore(LinkedListNode node, LinkedListNode newNode) + { + newNode.next = node; + newNode.prev = node.prev; + node.prev!.next = newNode; + node.prev = newNode; + version++; + count++; + } + + private void InternalInsertNodeToEmptyList(LinkedListNode newNode) + { + Debug.Assert(head == null && count == 0, "LinkedList must be empty when this method is called!"); + newNode.next = newNode; + newNode.prev = newNode; + head = newNode; + version++; + count++; + } + + internal void InternalRemoveNode(LinkedListNode node) + { + Debug.Assert(node.list == this, "Deleting the node from another list!"); + Debug.Assert(head != null, "This method shouldn't be called on empty list!"); + if (node.next == node) + { + Debug.Assert(count == 1 && head == node, "this should only be true for a list with only one node"); + head = null; + } + else + { + node.next!.prev = node.prev; + node.prev!.next = node.next; + if (head == node) + { + head = node.next; + } + } + node.Invalidate(); + count--; + version++; + } + + internal static void ValidateNewNode(LinkedListNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.list != null) + { + throw new InvalidOperationException(); + } + } + + internal void ValidateNode(LinkedListNode node) + { + if (node == null) + { + throw new ArgumentNullException(nameof(node)); + } + + if (node.list != this) + { + throw new InvalidOperationException(); + } + } + } + + // Note following class is not serializable since we customized the serialization of LinkedList. + internal sealed class LinkedListNode + { + internal LinkedList? list; + internal LinkedListNode? next; + internal LinkedListNode? prev; + internal T item; + + public LinkedListNode(T value) + { + item = value; + } + + public LinkedListNode? Next + { + get { return next == null || next == list!.head ? null : next; } + } + + public LinkedListNode? Previous + { + get { return prev == null || this == list!.head ? null : prev; } + } + + /// Gets a reference to the value held by the node. + public ref T ValueRef => ref item; + + internal void Invalidate() + { + list = null; + next = null; + prev = null; + } + } +} + +#endif diff --git a/GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs b/GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs new file mode 100644 index 00000000..2a692efd --- /dev/null +++ b/GameServer/KcpSharp/NetstandardShim/TaskCompletionSource.cs @@ -0,0 +1,11 @@ +#if NEED_TCS_SHIM + +namespace System.Threading.Tasks +{ + internal class TaskCompletionSource : TaskCompletionSource + { + public void TrySetResult() => TrySetResult(true); + } +} + +#endif diff --git a/GameServer/KcpSharp/ThrowHelper.cs b/GameServer/KcpSharp/ThrowHelper.cs new file mode 100644 index 00000000..da673154 --- /dev/null +++ b/GameServer/KcpSharp/ThrowHelper.cs @@ -0,0 +1,66 @@ +namespace EggLink.DanhengServer.KcpSharp +{ + internal static class ThrowHelper + { + public static void ThrowArgumentOutOfRangeException(string paramName) + { + throw new ArgumentOutOfRangeException(paramName); + } + public static void ThrowTransportClosedForStreanException() + { + throw new IOException("The underlying transport is closed."); + } + public static Exception NewMessageTooLargeForBufferArgument() + { + return new ArgumentException("Message is too large.", "buffer"); + } + public static Exception NewBufferTooSmallForBufferArgument() + { + return new ArgumentException("Buffer is too small.", "buffer"); + } + public static Exception ThrowBufferTooSmall() + { + throw new ArgumentException("Buffer is too small.", "buffer"); + } + public static Exception ThrowAllowPartialSendArgumentException() + { + throw new ArgumentException("allowPartialSend should not be set to true in non-stream mode.", "allowPartialSend"); + } + public static Exception NewArgumentOutOfRangeException(string paramName) + { + return new ArgumentOutOfRangeException(paramName); + } + public static Exception NewConcurrentSendException() + { + return new InvalidOperationException("Concurrent send operations are not allowed."); + } + public static Exception NewConcurrentReceiveException() + { + return new InvalidOperationException("Concurrent receive operations are not allowed."); + } + public static Exception NewTransportClosedForStreamException() + { + throw new IOException("The underlying transport is closed."); + } + public static Exception NewOperationCanceledExceptionForCancelPendingSend(Exception? innerException, CancellationToken cancellationToken) + { + return new OperationCanceledException("This operation is cancelled by a call to CancelPendingSend.", innerException, cancellationToken); + } + public static Exception NewOperationCanceledExceptionForCancelPendingReceive(Exception? innerException, CancellationToken cancellationToken) + { + return new OperationCanceledException("This operation is cancelled by a call to CancelPendingReceive.", innerException, cancellationToken); + } + public static void ThrowConcurrentReceiveException() + { + throw new InvalidOperationException("Concurrent receive operations are not allowed."); + } + public static Exception NewObjectDisposedForKcpStreamException() + { + return new ObjectDisposedException(nameof(KcpStream)); + } + public static void ThrowObjectDisposedForKcpStreamException() + { + throw new ObjectDisposedException(nameof(KcpStream)); + } + } +} diff --git a/GameServer/Program/EntryPoint.cs b/GameServer/Program/EntryPoint.cs index 2c62c5e1..5a4a8267 100644 --- a/GameServer/Program/EntryPoint.cs +++ b/GameServer/Program/EntryPoint.cs @@ -4,6 +4,8 @@ using EggLink.DanhengServer.Configuration; using EggLink.DanhengServer.WebServer; using EggLink.DanhengServer.Database; using System.Runtime.InteropServices; +using EggLink.DanhengServer.Server; +using EggLink.DanhengServer.Server.Packet; namespace EggLink.DanhengServer.Program { @@ -11,6 +13,9 @@ namespace EggLink.DanhengServer.Program { private static Logger logger = new("Program"); public static DatabaseHelper DatabaseHelper = new(); + public static Listener Listener = new(); + public static HandlerManager HandlerManager = new(); + public static void Main(string[] args) { var time = DateTime.Now; @@ -61,12 +66,14 @@ namespace EggLink.DanhengServer.Program Console.ReadLine(); return; } - WebProgram.Main([$"--urls=http://{GetConfig().HttpServer.PublicAddress}:{GetConfig().HttpServer.PublicPort}/"]); logger.Info($"DispatchServer is running on http://{GetConfig().HttpServer.PublicAddress}:{GetConfig().HttpServer.PublicPort}/"); - + + Listener.StartListener(); + var elapsed = DateTime.Now - time; logger.Info($"Done in {elapsed.TotalSeconds.ToString()[..4]}s! Type '/help' to get help of commands."); + while (true) { Console.ReadLine(); diff --git a/GameServer/Server/Connection.cs b/GameServer/Server/Connection.cs new file mode 100644 index 00000000..cff7df81 --- /dev/null +++ b/GameServer/Server/Connection.cs @@ -0,0 +1,235 @@ +using System.Buffers; +using System.Net; +using System.Reflection; +using EggLink.DanhengServer.Enums; +using EggLink.DanhengServer.Game.Player; +using EggLink.DanhengServer.KcpSharp; +using EggLink.DanhengServer.Program; +using EggLink.DanhengServer.Server.Packet; +using EggLink.DanhengServer.Util; +using Google.Protobuf; +using Google.Protobuf.Reflection; +using KcpSharp; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace EggLink.DanhengServer.Server; +public partial class Connection +{ + public long? ConversationID => Conversation.ConversationId; + private readonly KcpConversation Conversation; + private readonly CancellationTokenSource CancelToken; + public readonly IPEndPoint RemoteEndPoint; + public SessionState State { get; set; } = SessionState.INACTIVE; + private bool UseSecretKey { get; set; } = false; + private byte[] SecretKey = new byte[0x1000]; + public Player? Player { get; set; } + public uint ClientTime { get; private set; } + public long LastPingTime { get; private set; } + private uint LastClientSeq = 10; + public static readonly List BANNED_PACKETS = []; + private static Logger Logger = new("GameServer"); +#if DEBUG + private static uint LogIndex = 0; +#endif + public Connection(KcpConversation conversation, IPEndPoint remote) + { + Conversation = conversation; + RemoteEndPoint = remote; + CancelToken = new CancellationTokenSource(); + Start(); + } + + private async void Start() + { + Logger.Info($"New connection to {RemoteEndPoint} created with conversation id {Conversation.ConversationId}"); + State = SessionState.WAITING_FOR_TOKEN; + await ReceiveLoop(); + } + public async void Stop() + { + if (Player != null) + { + await Player.OnLogoutAsync(); + } + Listener.UnregisterConnection(this); + Conversation.Dispose(); + try + { + CancelToken.Cancel(); + CancelToken.Dispose(); + } + catch { } + + } + + private void UpdateLastPingTime(uint clientTime) + { + ClientTime = clientTime; + LastPingTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + } + +#if DEBUG + public static void LogPacket(string sendOrRecv, ushort opcode, byte[] payload) + { + //Logger.DebugWriteLine($"{sendOrRecv}: {Enum.GetName(typeof(OpCode), opcode)}({opcode})\r\n{Convert.ToHexString(payload)}"); + Type? typ = AppDomain.CurrentDomain.GetAssemblies(). + SingleOrDefault(assembly => assembly.GetName().Name == "Shared").GetTypes().First(t => t.Name == $"{Enum.GetName(typeof(CmdId), opcode)}"); //get the type using the packet name + MessageDescriptor? descriptor = (MessageDescriptor)typ.GetProperty("Descriptor", BindingFlags.Public | BindingFlags.Static).GetValue(null, null); // get the static property Descriptor + IMessage? packet = descriptor.Parser.ParseFrom(payload); + JsonFormatter? formatter = JsonFormatter.Default; + string? asJson = formatter.Format(packet); + Logger.Debug($"{sendOrRecv}: {Enum.GetName(typeof(CmdId), opcode)}({opcode})\r\n{asJson}"); + } + +#endif + private async Task ReceiveLoop() + { + while (!CancelToken.IsCancellationRequested) + { + // WaitToReceiveAsync call completes when there is at least one message is received or the transport is closed. + KcpConversationReceiveResult result = await Conversation.WaitToReceiveAsync(CancelToken.Token); + if (result.TransportClosed) + { + Logger.Debug("Connection was closed"); + break; + } + if (result.BytesReceived > Listener.MAX_MSG_SIZE) + { + // The message is too large. + Logger.Error("Packet too large"); + Conversation.SetTransportClosed(); + break; + } + + byte[] buffer = ArrayPool.Shared.Rent(result.BytesReceived); + try + { + // TryReceive should not return false here, unless the transport is closed. + // So we don't need to check for result.TransportClosed. + if (!Conversation.TryReceive(buffer, out result)) + { + Logger.Error("Failed to receive packet"); + break; + } + await ProcessMessageAsync(buffer.AsMemory(0, result.BytesReceived)); + } + catch (Exception ex) + { + Logger.Error("Packet parse error", ex); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + Stop(); + } + + // DO THE PROCESSING OF THE GAME PACKET + private async Task ProcessMessageAsync(Memory data) + { + byte[] gamePacket = data.ToArray(); + + // Decrypt and turn back into a packet + //Crypto.Xor(gamePacket, UseSecretKey ? SecretKey : Crypto.DISPATCH_KEY); + await using MemoryStream? ms = new(gamePacket); + using BinaryReader? br = new(ms); + + // Handle + try + { + while (br.BaseStream.Position < br.BaseStream.Length) + { + // Length + if (br.BaseStream.Length - br.BaseStream.Position < 12) + { + return; + } + // Packet sanity check + uint Magic1 = br.ReadUInt32BE(); + if (Magic1 != 0x9D74C714) + { + Logger.Error($"Bad Data Package Received: got 0x{Magic1:X}, expect 0x9D74C714"); + return; // Bad packet + } + // Data + ushort opcode = br.ReadUInt16BE(); + ushort headerLength = br.ReadUInt16BE(); + uint payloadLength = br.ReadUInt32BE(); + byte[] header = br.ReadBytes(headerLength); + byte[] payload = br.ReadBytes((int)payloadLength); + + await HandlePacketAsync(opcode, header, payload); + } + + } + catch (Exception e) + { + Logger.Error(e.Message, e); + } + finally + { + await ms.DisposeAsync(); + } + } + + private async Task HandlePacketAsync(ushort opcode, byte[] header, byte[] payload) + { + // Find the Handler for this opcode + Handler? handler = EntryPoint.HandlerManager.GetHandler(opcode); + if (handler != null) + { + // Handle + // Make sure session is ready for packets + SessionState state = State; + switch ((int)opcode) + { + case CmdId.PlayerGetTokenCsReq: + { + if (state != SessionState.WAITING_FOR_TOKEN) + { + return true; + } + goto default; + } + case CmdId.PlayerLoginCsReq: + { + if (state != SessionState.WAITING_FOR_LOGIN) + { + return true; + } + goto default; + } + default: + break; + } + handler.OnHandle(header, payload); + return true; + } + + return false; + } + + + public async Task SendPacketAsync(BasePacket packet) + { + // Test + if (packet.CmdId <= 0) + { + Logger.Debug("Tried to send packet with missing cmd id!"); + return; + } + + // DO NOT REMOVE (unless we find a way to validate code before sending to client which I don't think we can) + if (BANNED_PACKETS.Contains(packet.CmdId)) + { + return; + } + + // Header + byte[] packetBytes = packet.BuildPacket(); + + await Conversation.SendAsync(packetBytes, CancelToken.Token); + } +} diff --git a/GameServer/Server/GameSession.cs b/GameServer/Server/GameSession.cs new file mode 100644 index 00000000..25bb3b47 --- /dev/null +++ b/GameServer/Server/GameSession.cs @@ -0,0 +1,6 @@ +namespace EggLink.DanhengServer.Server +{ + public class GameSession() + { + } +} diff --git a/GameServer/Server/Listener.cs b/GameServer/Server/Listener.cs new file mode 100644 index 00000000..934148d4 --- /dev/null +++ b/GameServer/Server/Listener.cs @@ -0,0 +1,138 @@ +using System.Net; +using System.Net.Sockets; +using EggLink.DanhengServer.KcpSharp; +using EggLink.DanhengServer.Util; +using KcpSharp; + +namespace EggLink.DanhengServer.Server +{ + public class Listener + { + public const int MAX_MSG_SIZE = 16384; + public const int HANDSHAKE_SIZE = 20; + private static Socket? UDPListener => UDPClient?.Client; + private static UdpClient? UDPClient; + private static IPEndPoint? ListenAddress; + private static IKcpTransport? KCPTransport; + private static readonly CancellationTokenSource CancelToken = new(); + private static readonly Logger Logger = new("GameServer"); + private static IKcpMultiplexConnection? Multiplex => KCPTransport?.Connection; + public static readonly SortedList Connections = []; + public static Connection? GetConnectionByEndPoint(IPEndPoint ep) => Connections.Values.FirstOrDefault(c => c.RemoteEndPoint.Equals(ep)); + + private static readonly KcpConversationOptions ConvOpt = new() + { + StreamMode = false, + Mtu = 1400, + ReceiveWindow = 256, + SendWindow = 256, + NoDelay = true, + UpdateInterval = 100, + KeepAliveOptions = new KcpKeepAliveOptions(1000, 30000) + }; + private static uint PORT => ConfigManager.Config.GameServer.PublicPort; + public static void StartListener() + { + ListenAddress = new IPEndPoint(IPAddress.Parse(ConfigManager.Config.GameServer.PublicAddress), (int)PORT); + UDPClient = new UdpClient(ListenAddress); + if (UDPListener == null) return; + KCPTransport = KcpSocketTransport.CreateMultiplexConnection(UDPClient, 1400); + KCPTransport.Start(); + Logger.Info($"Game Server started. Listening on port: {PORT}"); + } + + private static void RegisterConnection(Connection con) + { + if (!con.ConversationID.HasValue) return; + Connections[con.ConversationID.Value] = con; + } + public static void UnregisterConnection(Connection con) + { + if (!con.ConversationID.HasValue) return; + long convId = con.ConversationID.Value; + if (Connections.Remove(convId)) + { + Multiplex?.UnregisterConversation(convId); + Logger.Info($"Connection with {con.RemoteEndPoint} has been closed"); + } + } + public static async Task HandleHandshake(UdpReceiveResult rcv) + { + try + { + Connection? con = GetConnectionByEndPoint(rcv.RemoteEndPoint); + await using MemoryStream? ms = new(rcv.Buffer); + using BinaryReader? br = new(ms); + int code = br.ReadInt32BE(); + br.ReadUInt32(); + br.ReadUInt32(); + int enet = br.ReadInt32BE(); + br.ReadUInt32(); + switch (code) + { + case 0x000000FF: + if (con != null) + { + Logger.Info($"Duplicate handshake from {con.RemoteEndPoint}"); + return; + } + await AcceptConnection(rcv, enet); + break; + case 0x00000194: + if (con == null) + { + Logger.Info($"Inexistent connection asked for disconnect from {rcv.RemoteEndPoint}"); + return; + } + await SendDisconnectPacket(con, 5); + break; + default: + Logger.Error($"Invalid handshake code received {code}"); + return; + } + } + catch (Exception ex) + { + Logger.Error($"Failed to handle handshake: {ex}"); + } + } + + private static async Task AcceptConnection(UdpReceiveResult rcv, int enet) + { + long convId = Connections.GetNextAvailableIndex(); + KcpConversation? convo = Multiplex?.CreateConversation(convId, rcv.RemoteEndPoint, ConvOpt); + if (convo == null) return; + Connection? con = new(convo, rcv.RemoteEndPoint); + RegisterConnection(con); + await SendHandshakeResponse(con, enet); + } + + private static async Task SendHandshakeResponse(Connection user, int enet) + { + if (user == null || UDPClient == null || !user.ConversationID.HasValue) return; + long convId = user.ConversationID.Value; + await using MemoryStream? ms = new(); + using BinaryWriter? bw = new(ms); + bw.WriteInt32BE(0x00000145); + bw.WriteConvID(convId); + bw.WriteInt32BE(enet); + bw.WriteInt32BE(0x14514545); + byte[]? data = ms.ToArray(); + await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint); + } + public static async Task SendDisconnectPacket(Connection user, int code) + { + if (user == null || UDPClient == null || !user.ConversationID.HasValue) return; + long convId = user.ConversationID.Value; + await using MemoryStream? ms = new(); + using BinaryWriter? bw = new(ms); + bw.WriteInt32BE(0x00000194); + bw.WriteConvID(convId); + bw.WriteInt32BE(code); + bw.WriteInt32BE(0x19419494); + byte[]? data = ms.ToArray(); + await UDPClient.SendAsync(data, data.Length, user.RemoteEndPoint); + } + + } +} diff --git a/GameServer/Server/Packet/BasePacket.cs b/GameServer/Server/Packet/BasePacket.cs new file mode 100644 index 00000000..474f778d --- /dev/null +++ b/GameServer/Server/Packet/BasePacket.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet +{ + public class BasePacket + { + public int CmdId; + + public byte[] BuildPacket() + { + return []; + } + } +} diff --git a/GameServer/Server/Packet/CmdId.cs b/GameServer/Server/Packet/CmdId.cs new file mode 100644 index 00000000..5431078e --- /dev/null +++ b/GameServer/Server/Packet/CmdId.cs @@ -0,0 +1,1164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet +{ + public class CmdId + { + // None + public const int None = 0; + + // Packet + public const int AcceptActivityExpeditionCsReq = 2546; + public const int AcceptActivityExpeditionScRsp = 2573; + public const int AcceptedPamMissionExpireCsReq = 4007; + public const int AcceptedPamMissionExpireScRsp = 4057; + public const int AcceptExpeditionCsReq = 2516; + public const int AcceptExpeditionScRsp = 2597; + public const int AcceptMainMissionCsReq = 1254; + public const int AcceptMainMissionScRsp = 1224; + public const int AcceptMissionEventCsReq = 1226; + public const int AcceptMissionEventScRsp = 1298; + public const int AceAntiCheaterCsReq = 78; + public const int AceAntiCheaterScRsp = 52; + public const int ActivateFarmElementCsReq = 1430; + public const int ActivateFarmElementScRsp = 1475; + public const int AddAvatarScNotify = 323; + public const int AddBlacklistCsReq = 2918; + public const int AddBlacklistScRsp = 2939; + public const int AddEquipmentScNotify = 512; + public const int AetherDivideFinishChallengeScNotify = 4895; + public const int AetherDivideLineupScNotify = 4824; + public const int AetherDivideRefreshEndlessCsReq = 4817; + public const int AetherDivideRefreshEndlessScNotify = 4855; + public const int AetherDivideRefreshEndlessScRsp = 4894; + public const int AetherDivideSkillItemScNotify = 4810; + public const int AetherDivideSpiritExpUpCsReq = 4851; + public const int AetherDivideSpiritExpUpScRsp = 4853; + public const int AetherDivideSpiritInfoScNotify = 4876; + public const int AetherDivideTainerInfoScNotify = 4879; + public const int AetherDivideTakeChallengeRewardCsReq = 4815; + public const int AetherDivideTakeChallengeRewardScRsp = 4805; + public const int AlleyEventChangeNotify = 4800; + public const int AlleyEventEffectNotify = 4746; + public const int AlleyFundsScNotify = 4751; + public const int AlleyGuaranteedFundsCsReq = 4717; + public const int AlleyGuaranteedFundsScRsp = 4794; + public const int AlleyOrderChangedScNotify = 4798; + public const int AlleyPlacingGameCsReq = 4723; + public const int AlleyPlacingGameScRsp = 4789; + public const int AlleyShipmentEventEffectsScNotify = 4779; + public const int AlleyShipUnlockScNotify = 4776; + public const int AlleyShipUsedCountScNotify = 4724; + public const int AlleyShopLevelScNotify = 4753; + public const int AlleyTakeEventRewardCsReq = 4755; + public const int AlleyTakeEventRewardScRsp = 4715; + public const int AntiAddictScNotify = 98; + public const int ApplyFriendCsReq = 2971; + public const int ApplyFriendScRsp = 2934; + public const int ArchiveWolfBroGameCsReq = 6516; + public const int ArchiveWolfBroGameScRsp = 6597; + public const int AvatarExpUpCsReq = 316; + public const int AvatarExpUpScRsp = 397; + public const int BatchGetQuestDataCsReq = 981; + public const int BatchGetQuestDataScRsp = 918; + public const int BatchMarkChatEmojiCsReq = 3923; + public const int BatchMarkChatEmojiScRsp = 3989; + public const int BattleCollegeDataChangeScNotify = 5716; + public const int BattleLogReportCsReq = 146; + public const int BattleLogReportScRsp = 173; + public const int BattlePassInfoNotify = 3007; + public const int BoxingClubChallengeUpdateScNotify = 4246; + public const int BoxingClubRewardScNotify = 4300; + public const int BuyBpLevelCsReq = 3019; + public const int BuyBpLevelScRsp = 3071; + public const int BuyGoodsCsReq = 1516; + public const int BuyGoodsScRsp = 1597; + public const int BuyNpcStuffCsReq = 4316; + public const int BuyNpcStuffScRsp = 4397; + public const int BuyRogueShopBuffCsReq = 5646; + public const int BuyRogueShopBuffScRsp = 5673; + public const int BuyRogueShopMiracleCsReq = 5634; + public const int BuyRogueShopMiracleScRsp = 5700; + public const int CancelActivityExpeditionCsReq = 2506; + public const int CancelActivityExpeditionScRsp = 2523; + public const int CancelCacheNotifyCsReq = 4200; + public const int CancelCacheNotifyScRsp = 4146; + public const int CancelExpeditionCsReq = 2504; + public const int CancelExpeditionScRsp = 2519; + public const int CancelMarkItemNotify = 505; + public const int ChallengeLineupNotify = 1706; + public const int ChallengeRaidNotify = 2246; + public const int ChallengeSettleNotify = 1771; + public const int ChangeLineupLeaderCsReq = 789; + public const int ChangeLineupLeaderScRsp = 781; + public const int ChangeScriptEmotionCsReq = 6316; + public const int ChangeScriptEmotionScRsp = 6397; + public const int ChangeStoryLineCsReq = 6297; + public const int ChangeStoryLineFinishScNotify = 6219; + public const int ChangeStoryLineScRsp = 6204; + public const int ChessRogueCellUpdateNotify = 5493; + public const int ChessRogueChangeyAeonDimensionNotify = 5589; + public const int ChessRogueCheatRollCsReq = 5534; + public const int ChessRogueCheatRollScRsp = 5586; + public const int ChessRogueConfirmRollCsReq = 5596; + public const int ChessRogueConfirmRollScRsp = 5590; + public const int ChessRogueEnterCellCsReq = 5423; + public const int ChessRogueEnterCellScRsp = 5478; + public const int ChessRogueEnterCsReq = 5511; + public const int ChessRogueEnterNextLayerCsReq = 5456; + public const int ChessRogueEnterNextLayerScRsp = 5443; + public const int ChessRogueEnterScRsp = 5527; + public const int ChessRogueFinishCurRoomNotify = 5429; + public const int ChessRogueGiveUpCsReq = 5405; + public const int ChessRogueGiveUpRollCsReq = 5460; + public const int ChessRogueGiveUpRollScRsp = 5550; + public const int ChessRogueGiveUpScRsp = 5503; + public const int ChessRogueGoAheadCsReq = 5588; + public const int ChessRogueGoAheadScRsp = 5416; + public const int ChessRogueLayerAccountInfoNotify = 5507; + public const int ChessRogueLeaveCsReq = 5450; + public const int ChessRogueLeaveScRsp = 5468; + public const int ChessRogueMoveCellNotify = 5598; + public const int ChessRogueNousDiceSurfaceUnlockNotify = 5520; + public const int ChessRogueNousDiceUpdateNotify = 5409; + public const int ChessRogueNousEditDiceCsReq = 5407; + public const int ChessRogueNousEditDiceScRsp = 5452; + public const int ChessRogueNousEnableRogueTalentCsReq = 5481; + public const int ChessRogueNousEnableRogueTalentScRsp = 5515; + public const int ChessRogueNousGetRogueTalentInfoCsReq = 5509; + public const int ChessRogueNousGetRogueTalentInfoScRsp = 5571; + public const int ChessRoguePickAvatarCsReq = 5564; + public const int ChessRoguePickAvatarScRsp = 5541; + public const int ChessRogueQueryAeonDimensionsCsReq = 5424; + public const int ChessRogueQueryAeonDimensionsScRsp = 5492; + public const int ChessRogueQueryBpCsReq = 5422; + public const int ChessRogueQueryBpScRsp = 5595; + public const int ChessRogueQueryCsReq = 5461; + public const int ChessRogueQueryScRsp = 5522; + public const int ChessRogueQuestFinishNotify = 5510; + public const int ChessRogueQuitCsReq = 5523; + public const int ChessRogueQuitScRsp = 5415; + public const int ChessRogueReRollDiceCsReq = 5419; + public const int ChessRogueReRollDiceScRsp = 5475; + public const int ChessRogueReviveAvatarCsReq = 5496; + public const int ChessRogueReviveAvatarScRsp = 5539; + public const int ChessRogueRollDiceCsReq = 5425; + public const int ChessRogueRollDiceScRsp = 5454; + public const int ChessRogueSelectBpCsReq = 5524; + public const int ChessRogueSelectBpScRsp = 5497; + public const int ChessRogueSelectCellCsReq = 5435; + public const int ChessRogueSelectCellScRsp = 5474; + public const int ChessRogueSkipTeachingLevelCsReq = 5506; + public const int ChessRogueSkipTeachingLevelScRsp = 5411; + public const int ChessRogueStartCsReq = 5446; + public const int ChessRogueStartScRsp = 5529; + public const int ChessRogueUpdateActionPointScNotify = 5532; + public const int ChessRogueUpdateAeonModifierValueScNotify = 5483; + public const int ChessRogueUpdateAllowedSelectCellScNotify = 5566; + public const int ChessRogueUpdateBoardScNotify = 5572; + public const int ChessRogueUpdateDiceInfoScNotify = 5447; + public const int ChessRogueUpdateDicePassiveAccumulateValueScNotify = 5420; + public const int ChessRogueUpdateLevelBaseInfoScNotify = 5542; + public const int ChessRogueUpdateMoneyInfoScNotify = 5559; + public const int ChessRogueUpdateReviveInfoScNotify = 5570; + public const int ChessRogueUpdateUnlockLevelScNotify = 5545; + public const int ChooseBoxingClubResonanceCsReq = 4273; + public const int ChooseBoxingClubResonanceScRsp = 4206; + public const int ChooseBoxingClubStageOptionalBuffCsReq = 4281; + public const int ChooseBoxingClubStageOptionalBuffScRsp = 4218; + public const int CityShopInfoScNotify = 1571; + public const int ClearAetherDividePassiveSkillCsReq = 4839; + public const int ClearAetherDividePassiveSkillScRsp = 4826; + public const int ClientDownloadDataScNotify = 30; + public const int ClientObjDownloadDataScNotify = 69; + public const int ClientObjUploadCsReq = 86; + public const int ClientObjUploadScRsp = 1; + public const int CommonRogueQueryCsReq = 5642; + public const int CommonRogueQueryScRsp = 5627; + public const int CommonRogueUpdateScNotify = 5635; + public const int ComposeItemCsReq = 523; + public const int ComposeItemScRsp = 589; + public const int ComposeLimitNumCompleteNotify = 579; + public const int ComposeLimitNumUpdateNotify = 510; + public const int ComposeSelectedRelicCsReq = 553; + public const int ComposeSelectedRelicScRsp = 576; + public const int CurAssistChangedNotify = 2911; + public const int CurTrialActivityScNotify = 2621; + public const int DailyActiveInfoNotify = 3304; + public const int DailyFirstMeetPamCsReq = 3416; + public const int DailyFirstMeetPamScRsp = 3497; + public const int DailyRefreshNotify = 80; + public const int DailyTaskDataScNotify = 1234; + public const int DeactivateFarmElementCsReq = 1490; + public const int DeactivateFarmElementScRsp = 1438; + public const int DeleteBlacklistCsReq = 2976; + public const int DeleteBlacklistScRsp = 2912; + public const int DeleteFriendCsReq = 2923; + public const int DeleteFriendScRsp = 2989; + public const int DeleteSummonUnitCsReq = 1487; + public const int DeleteSummonUnitScRsp = 1461; + public const int DelMailCsReq = 804; + public const int DelMailScRsp = 819; + public const int DelSaveRaidScNotify = 2298; + public const int DeployRotaterCsReq = 6804; + public const int DeployRotaterScRsp = 6819; + public const int DestroyItemCsReq = 554; + public const int DestroyItemScRsp = 524; + public const int DiscardRelicCsReq = 513; + public const int DiscardRelicScRsp = 590; + public const int DoGachaCsReq = 1916; + public const int DoGachaInRollShopCsReq = 6920; + public const int DoGachaInRollShopScRsp = 6902; + public const int DoGachaScRsp = 1997; + public const int DressAvatarCsReq = 400; + public const int DressAvatarScRsp = 346; + public const int DressAvatarSkinCsReq = 365; + public const int DressAvatarSkinScRsp = 351; + public const int DressRelicAvatarCsReq = 318; + public const int DressRelicAvatarScRsp = 339; + public const int EnableRogueTalentCsReq = 1849; + public const int EnableRogueTalentScRsp = 1887; + public const int EnhanceChessRogueBuffCsReq = 5472; + public const int EnhanceChessRogueBuffScRsp = 5470; + public const int EnhanceCommonRogueBuffCsReq = 5651; + public const int EnhanceCommonRogueBuffScRsp = 5653; + public const int EnhanceRogueBuffCsReq = 1876; + public const int EnhanceRogueBuffScRsp = 1812; + public const int EnterAdventureCsReq = 1307; + public const int EnterAdventureScRsp = 1357; + public const int EnterAetherDivideSceneCsReq = 4807; + public const int EnterAetherDivideSceneScRsp = 4857; + public const int EnterChessRogueAeonRoomCsReq = 5477; + public const int EnterChessRogueAeonRoomScRsp = 5500; + public const int EnteredSceneChangeScNotify = 1491; + public const int EnterFantasticStoryActivityStageCsReq = 4997; + public const int EnterFantasticStoryActivityStageScRsp = 4904; + public const int EnterFightActivityStageCsReq = 3697; + public const int EnterFightActivityStageScRsp = 3604; + public const int EnterMapRotationRegionCsReq = 6807; + public const int EnterMapRotationRegionScRsp = 6857; + public const int EnterRogueCsReq = 1804; + public const int EnterRogueEndlessActivityStageCsReq = 6016; + public const int EnterRogueEndlessActivityStageScRsp = 6097; + public const int EnterRogueMapRoomCsReq = 1808; + public const int EnterRogueMapRoomScRsp = 1811; + public const int EnterRogueScRsp = 1819; + public const int EnterSceneByServerScNotify = 1492; + public const int EnterSceneCsReq = 1422; + public const int EnterSceneScRsp = 1493; + public const int EnterSectionCsReq = 1485; + public const int EnterSectionScRsp = 1495; + public const int EnterStrongChallengeActivityStageCsReq = 6616; + public const int EnterStrongChallengeActivityStageScRsp = 6697; + public const int EnterTelevisionActivityStageCsReq = 6962; + public const int EnterTelevisionActivityStageScRsp = 6963; + public const int EnterTreasureDungeonCsReq = 4423; + public const int EnterTreasureDungeonScRsp = 4489; + public const int EnterTrialActivityStageCsReq = 2652; + public const int EnterTrialActivityStageScRsp = 2642; + public const int EntityBindPropCsReq = 1443; + public const int EntityBindPropScRsp = 1408; + public const int EquipAetherDividePassiveSkillCsReq = 4881; + public const int EquipAetherDividePassiveSkillScRsp = 4818; + public const int ExchangeGachaCeilingCsReq = 1971; + public const int ExchangeGachaCeilingScRsp = 1934; + public const int ExchangeHcoinCsReq = 565; + public const int ExchangeHcoinScRsp = 551; + public const int ExchangeRogueBuffWithMiracleCsReq = 5698; + public const int ExchangeRogueBuffWithMiracleScRsp = 5660; + public const int ExchangeRogueRewardKeyCsReq = 1835; + public const int ExchangeRogueRewardKeyScRsp = 1870; + public const int ExchangeStaminaCsReq = 89; + public const int ExchangeStaminaScRsp = 81; + public const int ExpeditionDataChangeScNotify = 2600; + public const int ExpUpEquipmentCsReq = 573; + public const int ExpUpEquipmentScRsp = 506; + public const int ExpUpRelicCsReq = 581; + public const int ExpUpRelicScRsp = 518; + public const int ExtraLineupDestroyNotify = 776; + public const int FantasticStoryActivityBattleEndScNotify = 4919; + public const int FeatureSwitchClosedScNotify = 28; + public const int FightActivityDataChangeScNotify = 3616; + public const int FightTreasureDungeonMonsterCsReq = 4439; + public const int FightTreasureDungeonMonsterScRsp = 4426; + public const int FinishAeonDialogueGroupCsReq = 1882; + public const int FinishAeonDialogueGroupScRsp = 1891; + public const int FinishChapterScNotify = 4916; + public const int FinishChessRogueNousSubStoryCsReq = 5577; + public const int FinishChessRogueNousSubStoryScRsp = 5508; + public const int FinishChessRogueSubStoryCsReq = 5421; + public const int FinishChessRogueSubStoryScRsp = 5476; + public const int FinishCosumeItemMissionCsReq = 1223; + public const int FinishCosumeItemMissionScRsp = 1289; + public const int FinishCurTurnCsReq = 4373; + public const int FinishCurTurnScRsp = 4306; + public const int FinishEmotionDialoguePerformanceCsReq = 6371; + public const int FinishEmotionDialoguePerformanceScRsp = 6334; + public const int FinishFirstTalkByPerformanceNpcCsReq = 2123; + public const int FinishFirstTalkByPerformanceNpcScRsp = 2189; + public const int FinishFirstTalkNpcCsReq = 2171; + public const int FinishFirstTalkNpcScRsp = 2134; + public const int FinishItemIdCsReq = 2704; + public const int FinishItemIdScRsp = 2719; + public const int FinishPerformSectionIdCsReq = 2800; + public const int FinishPerformSectionIdScRsp = 2746; + public const int FinishPlotCsReq = 1107; + public const int FinishPlotScRsp = 1157; + public const int FinishQuestCsReq = 946; + public const int FinishQuestScRsp = 973; + public const int FinishRogueDialogueGroupCsReq = 1878; + public const int FinishRogueDialogueGroupScRsp = 1852; + public const int FinishSectionIdCsReq = 2771; + public const int FinishSectionIdScRsp = 2734; + public const int FinishTalkMissionCsReq = 1216; + public const int FinishTalkMissionScRsp = 1297; + public const int FinishTutorialCsReq = 1700; + public const int FinishTutorialGuideCsReq = 1673; + public const int FinishTutorialGuideScRsp = 1606; + public const int FinishTutorialScRsp = 1646; + public const int GameplayCounterCountDownCsReq = 1469; + public const int GameplayCounterCountDownScRsp = 1486; + public const int GameplayCounterRecoverCsReq = 1403; + public const int GameplayCounterRecoverScRsp = 1496; + public const int GameplayCounterUpdateScNotify = 1401; + public const int GateServerScNotify = 45; + public const int GeneralVirtualItemDataNotify = 502; + public const int GetActivityScheduleConfigCsReq = 2604; + public const int GetActivityScheduleConfigScRsp = 2619; + public const int GetAetherDivideChallengeInfoCsReq = 4812; + public const int GetAetherDivideChallengeInfoScRsp = 4885; + public const int GetAetherDivideInfoCsReq = 4873; + public const int GetAetherDivideInfoScRsp = 4806; + public const int GetAlleyInfoCsReq = 4707; + public const int GetAlleyInfoScRsp = 4757; + public const int GetAllLineupDataCsReq = 760; + public const int GetAllLineupDataScRsp = 750; + public const int GetAllRedDotDataCsReq = 5907; + public const int GetAllRedDotDataScRsp = 5957; + public const int GetAllSaveRaidCsReq = 2239; + public const int GetAllSaveRaidScRsp = 2226; + public const int GetAllServerPrefsDataCsReq = 6107; + public const int GetAllServerPrefsDataScRsp = 6157; + public const int GetArchiveDataCsReq = 2307; + public const int GetArchiveDataScRsp = 2357; + public const int GetAssistHistoryCsReq = 2955; + public const int GetAssistHistoryScRsp = 2915; + public const int GetAssistListCsReq = 2979; + public const int GetAssistListScRsp = 2910; + public const int GetAuthkeyCsReq = 18; + public const int GetAuthkeyScRsp = 39; + public const int GetAvatarDataCsReq = 307; + public const int GetAvatarDataScRsp = 357; + public const int GetBagCsReq = 507; + public const int GetBagScRsp = 557; + public const int GetBasicInfoCsReq = 88; + public const int GetBasicInfoScRsp = 37; + public const int GetBattleCollegeDataCsReq = 5707; + public const int GetBattleCollegeDataScRsp = 5757; + public const int GetBoxingClubInfoCsReq = 4207; + public const int GetBoxingClubInfoScRsp = 4257; + public const int GetChallengeCsReq = 1707; + public const int GetChallengeGroupStatisticsCsReq = 1739; + public const int GetChallengeGroupStatisticsScRsp = 1726; + public const int GetChallengeRaidInfoCsReq = 2219; + public const int GetChallengeRaidInfoScRsp = 2271; + public const int GetChallengeScRsp = 1757; + public const int GetChapterCsReq = 404; + public const int GetChapterScRsp = 419; + public const int GetChatEmojiListCsReq = 4000; + public const int GetChatEmojiListScRsp = 3946; + public const int GetChatFriendHistoryCsReq = 3971; + public const int GetChatFriendHistoryScRsp = 3934; + public const int GetChessRogueBuffEnhanceInfoCsReq = 5516; + public const int GetChessRogueBuffEnhanceInfoScRsp = 5579; + public const int GetChessRogueNousStoryInfoCsReq = 5538; + public const int GetChessRogueNousStoryInfoScRsp = 5587; + public const int GetChessRogueStoryAeonTalkInfoCsReq = 5593; + public const int GetChessRogueStoryAeonTalkInfoScRsp = 5430; + public const int GetChessRogueStoryInfoCsReq = 5440; + public const int GetChessRogueStoryInfoScRsp = 5401; + public const int GetCurAssistCsReq = 2917; + public const int GetCurAssistScRsp = 2994; + public const int GetCurBattleInfoCsReq = 104; + public const int GetCurBattleInfoScRsp = 119; + public const int GetCurChallengeCsReq = 1746; + public const int GetCurChallengeScRsp = 1773; + public const int GetCurLineupDataCsReq = 716; + public const int GetCurLineupDataScRsp = 797; + public const int GetCurSceneInfoCsReq = 1471; + public const int GetCurSceneInfoScRsp = 1434; + public const int GetDailyActiveInfoCsReq = 3316; + public const int GetDailyActiveInfoScRsp = 3397; + public const int GetEnhanceCommonRogueBuffInfoCsReq = 5650; + public const int GetEnhanceCommonRogueBuffInfoScRsp = 5665; + public const int GetEnteredSceneCsReq = 1444; + public const int GetEnteredSceneScRsp = 1482; + public const int GetExhibitScNotify = 4346; + public const int GetExpeditionDataCsReq = 2507; + public const int GetExpeditionDataScRsp = 2557; + public const int GetFantasticStoryActivityDataCsReq = 4907; + public const int GetFantasticStoryActivityDataScRsp = 4957; + public const int GetFarmStageGachaInfoCsReq = 1316; + public const int GetFarmStageGachaInfoScRsp = 1397; + public const int GetFightActivityDataCsReq = 3607; + public const int GetFightActivityDataScRsp = 3657; + public const int GetFirstTalkByPerformanceNpcCsReq = 2173; + public const int GetFirstTalkByPerformanceNpcScRsp = 2106; + public const int GetFirstTalkNpcCsReq = 2104; + public const int GetFirstTalkNpcScRsp = 2119; + public const int GetFriendApplyListInfoCsReq = 2904; + public const int GetFriendApplyListInfoScRsp = 2919; + public const int GetFriendAssistListCsReq = 2980; + public const int GetFriendAssistListScRsp = 2936; + public const int GetFriendListInfoCsReq = 2907; + public const int GetFriendListInfoScRsp = 2957; + public const int GetFriendLoginInfoCsReq = 2990; + public const int GetFriendLoginInfoScRsp = 2938; + public const int GetFriendRecommendListInfoCsReq = 2998; + public const int GetFriendRecommendListInfoScRsp = 2960; + public const int GetGachaCeilingCsReq = 1904; + public const int GetGachaCeilingScRsp = 1919; + public const int GetGachaInfoCsReq = 1907; + public const int GetGachaInfoScRsp = 1957; + public const int GetHeartDialInfoCsReq = 6307; + public const int GetHeartDialInfoScRsp = 6357; + public const int GetHeroBasicTypeInfoCsReq = 17; + public const int GetHeroBasicTypeInfoScRsp = 94; + public const int GetJukeboxDataCsReq = 3107; + public const int GetJukeboxDataScRsp = 3157; + public const int GetKilledPunkLordMonsterDataCsReq = 3253; + public const int GetKilledPunkLordMonsterDataScRsp = 3276; + public const int GetLevelRewardCsReq = 53; + public const int GetLevelRewardScRsp = 76; + public const int GetLevelRewardTakenListCsReq = 65; + public const int GetLevelRewardTakenListScRsp = 51; + public const int GetLineupAvatarDataCsReq = 706; + public const int GetLineupAvatarDataScRsp = 723; + public const int GetLoginActivityCsReq = 2607; + public const int GetLoginActivityScRsp = 2657; + public const int GetLoginChatInfoCsReq = 3981; + public const int GetLoginChatInfoScRsp = 3918; + public const int GetMailCsReq = 807; + public const int GetMailScRsp = 857; + public const int GetMainMissionCustomValueCsReq = 1217; + public const int GetMainMissionCustomValueScRsp = 1294; + public const int GetMapRotationDataCsReq = 6873; + public const int GetMapRotationDataScRsp = 6806; + public const int GetMarkItemListCsReq = 517; + public const int GetMarkItemListScRsp = 594; + public const int GetMissionDataCsReq = 1207; + public const int GetMissionDataScRsp = 1257; + public const int GetMissionEventDataCsReq = 1281; + public const int GetMissionEventDataScRsp = 1218; + public const int GetMissionStatusCsReq = 1260; + public const int GetMissionStatusScRsp = 1250; + public const int GetMonsterResearchActivityDataCsReq = 2639; + public const int GetMonsterResearchActivityDataScRsp = 2626; + public const int GetMultipleDropInfoCsReq = 4607; + public const int GetMultipleDropInfoScRsp = 4657; + public const int GetMuseumInfoCsReq = 4307; + public const int GetMuseumInfoScRsp = 4357; + public const int GetNpcMessageGroupCsReq = 2707; + public const int GetNpcMessageGroupScRsp = 2757; + public const int GetNpcStatusCsReq = 2716; + public const int GetNpcStatusScRsp = 2797; + public const int GetNpcTakenRewardCsReq = 2107; + public const int GetNpcTakenRewardScRsp = 2157; + public const int GetOfferingInfoCsReq = 6925; + public const int GetOfferingInfoScRsp = 6921; + public const int GetPhoneDataCsReq = 5107; + public const int GetPhoneDataScRsp = 5157; + public const int GetPlatformPlayerInfoCsReq = 2902; + public const int GetPlatformPlayerInfoScRsp = 2913; + public const int GetPlayerBoardDataCsReq = 2807; + public const int GetPlayerBoardDataScRsp = 2857; + public const int GetPlayerDetailInfoCsReq = 2916; + public const int GetPlayerDetailInfoScRsp = 2997; + public const int GetPlayerReplayInfoCsReq = 3516; + public const int GetPlayerReplayInfoScRsp = 3597; + public const int GetPlayerReturnMultiDropInfoCsReq = 4697; + public const int GetPlayerReturnMultiDropInfoScRsp = 4604; + public const int GetPrivateChatHistoryCsReq = 3904; + public const int GetPrivateChatHistoryScRsp = 3919; + public const int GetPunkLordBattleRecordCsReq = 3224; + public const int GetPunkLordBattleRecordScRsp = 3217; + public const int GetPunkLordDataCsReq = 3218; + public const int GetPunkLordDataScRsp = 3239; + public const int GetPunkLordMonsterDataCsReq = 3207; + public const int GetPunkLordMonsterDataScRsp = 3257; + public const int GetQuestDataCsReq = 907; + public const int GetQuestDataScRsp = 957; + public const int GetQuestRecordCsReq = 971; + public const int GetQuestRecordScRsp = 934; + public const int GetRaidInfoCsReq = 2273; + public const int GetRaidInfoScRsp = 2206; + public const int GetRecyleTimeCsReq = 585; + public const int GetRecyleTimeScRsp = 595; + public const int GetReplayTokenCsReq = 3507; + public const int GetReplayTokenScRsp = 3557; + public const int GetRndOptionCsReq = 3407; + public const int GetRndOptionScRsp = 3457; + public const int GetRogueAdventureRoomInfoCsReq = 5689; + public const int GetRogueAdventureRoomInfoScRsp = 5681; + public const int GetRogueAeonInfoCsReq = 1825; + public const int GetRogueAeonInfoScRsp = 1844; + public const int GetRogueBuffEnhanceInfoCsReq = 1851; + public const int GetRogueBuffEnhanceInfoScRsp = 1853; + public const int GetRogueDialogueEventDataCsReq = 1836; + public const int GetRogueDialogueEventDataScRsp = 1872; + public const int GetRogueEndlessActivityDataCsReq = 6007; + public const int GetRogueEndlessActivityDataScRsp = 6057; + public const int GetRogueHandbookDataCsReq = 5605; + public const int GetRogueHandbookDataScRsp = 5643; + public const int GetRogueInfoCsReq = 1807; + public const int GetRogueInfoScRsp = 1857; + public const int GetRogueInitialScoreCsReq = 1802; + public const int GetRogueInitialScoreScRsp = 1813; + public const int GetRogueScoreRewardInfoCsReq = 1869; + public const int GetRogueScoreRewardInfoScRsp = 1886; + public const int GetRogueShopBuffInfoCsReq = 5619; + public const int GetRogueShopBuffInfoScRsp = 5671; + public const int GetRogueShopMiracleInfoCsReq = 5697; + public const int GetRogueShopMiracleInfoScRsp = 5604; + public const int GetRogueTalentInfoCsReq = 1856; + public const int GetRogueTalentInfoScRsp = 1868; + public const int GetRollShopInfoCsReq = 6905; + public const int GetRollShopInfoScRsp = 6901; + public const int GetSaveLogisticsMapCsReq = 4710; + public const int GetSaveLogisticsMapScRsp = 4754; + public const int GetSaveRaidCsReq = 2281; + public const int GetSaveRaidScRsp = 2218; + public const int GetSceneMapInfoCsReq = 1409; + public const int GetSceneMapInfoScRsp = 1429; + public const int GetSecretKeyInfoCsReq = 48; + public const int GetSecretKeyInfoScRsp = 62; + public const int GetServerPrefsDataCsReq = 6116; + public const int GetServerPrefsDataScRsp = 6197; + public const int GetShareDataCsReq = 4116; + public const int GetShareDataScRsp = 4197; + public const int GetShopListCsReq = 1507; + public const int GetShopListScRsp = 1557; + public const int GetSingleRedDotParamGroupCsReq = 5904; + public const int GetSingleRedDotParamGroupScRsp = 5919; + public const int GetSpringRecoverDataCsReq = 1488; + public const int GetSpringRecoverDataScRsp = 1437; + public const int GetStageLineupCsReq = 707; + public const int GetStageLineupScRsp = 757; + public const int GetStoryLineInfoCsReq = 6207; + public const int GetStoryLineInfoScRsp = 6257; + public const int GetStrongChallengeActivityDataCsReq = 6607; + public const int GetStrongChallengeActivityDataScRsp = 6657; + public const int GetStuffScNotify = 4400; + public const int GetTelevisionActivityDataCsReq = 6965; + public const int GetTelevisionActivityDataScRsp = 6961; + public const int GetTrainVisitorBehaviorCsReq = 3716; + public const int GetTrainVisitorBehaviorScRsp = 3797; + public const int GetTrainVisitorRegisterCsReq = 3771; + public const int GetTrainVisitorRegisterScRsp = 3734; + public const int GetTreasureDungeonActivityDataCsReq = 4473; + public const int GetTreasureDungeonActivityDataScRsp = 4406; + public const int GetTrialActivityDataCsReq = 2636; + public const int GetTrialActivityDataScRsp = 2672; + public const int GetTutorialCsReq = 1607; + public const int GetTutorialGuideCsReq = 1616; + public const int GetTutorialGuideScRsp = 1697; + public const int GetTutorialScRsp = 1657; + public const int GetUnlockTeleportCsReq = 1463; + public const int GetUnlockTeleportScRsp = 1466; + public const int GetUpdatedArchiveDataCsReq = 2316; + public const int GetUpdatedArchiveDataScRsp = 2397; + public const int GetVideoVersionKeyCsReq = 93; + public const int GetVideoVersionKeyScRsp = 92; + public const int GetWaypointCsReq = 407; + public const int GetWaypointScRsp = 457; + public const int GetWolfBroGameDataCsReq = 6600; + public const int GetWolfBroGameDataScRsp = 6546; + public const int GiveUpBoxingClubChallengeCsReq = 4271; + public const int GiveUpBoxingClubChallengeScRsp = 4234; + public const int GmTalkCsReq = 46; + public const int GmTalkScNotify = 34; + public const int GmTalkScRsp = 73; + public const int GroupStateChangeCsReq = 1414; + public const int GroupStateChangeScNotify = 1425; + public const int GroupStateChangeScRsp = 1447; + public const int HandleFriendCsReq = 2946; + public const int HandleFriendScRsp = 2973; + public const int HandleRogueCommonPendingActionCsReq = 5678; + public const int HandleRogueCommonPendingActionScRsp = 5652; + public const int HealPoolInfoNotify = 1452; + public const int HeartDialScriptChangeScNotify = 6400; + public const int HeartDialTraceScriptCsReq = 6346; + public const int HeartDialTraceScriptScRsp = 6373; + public const int HeliobusActivityDataCsReq = 5807; + public const int HeliobusActivityDataScRsp = 5857; + public const int HeliobusChallengeUpdateScNotify = 5853; + public const int HeliobusEnterBattleCsReq = 5860; + public const int HeliobusEnterBattleScRsp = 5850; + public const int HeliobusInfoChangedScNotify = 5806; + public const int HeliobusLineupUpdateScNotify = 5876; + public const int HeliobusSelectSkillCsReq = 5818; + public const int HeliobusSelectSkillScRsp = 5839; + public const int HeliobusSnsCommentCsReq = 5900; + public const int HeliobusSnsCommentScRsp = 5846; + public const int HeliobusSnsLikeCsReq = 5871; + public const int HeliobusSnsLikeScRsp = 5834; + public const int HeliobusSnsPostCsReq = 5804; + public const int HeliobusSnsPostScRsp = 5819; + public const int HeliobusSnsReadCsReq = 5816; + public const int HeliobusSnsReadScRsp = 5897; + public const int HeliobusSnsUpdateScNotify = 5873; + public const int HeliobusStartRaidCsReq = 5865; + public const int HeliobusStartRaidScRsp = 5851; + public const int HeliobusUnlockSkillScNotify = 5881; + public const int HeliobusUpgradeLevelCsReq = 5823; + public const int HeliobusUpgradeLevelScRsp = 5889; + public const int HeroBasicTypeChangedNotify = 13; + public const int InteractChargerCsReq = 6816; + public const int InteractChargerScRsp = 6897; + public const int InteractPropCsReq = 1416; + public const int InteractPropScRsp = 1497; + public const int InteractTreasureDungeonGridCsReq = 4498; + public const int InteractTreasureDungeonGridScRsp = 4460; + public const int InterruptMissionEventCsReq = 1265; + public const int InterruptMissionEventScRsp = 1251; + public const int JoinLineupCsReq = 704; + public const int JoinLineupScRsp = 719; + public const int LastSpringRefreshTimeNotify = 1460; + public const int LeaveAetherDivideSceneCsReq = 4816; + public const int LeaveAetherDivideSceneScRsp = 4897; + public const int LeaveChallengeCsReq = 1704; + public const int LeaveChallengeScRsp = 1719; + public const int LeaveMapRotationRegionCsReq = 6900; + public const int LeaveMapRotationRegionScNotify = 6881; + public const int LeaveMapRotationRegionScRsp = 6846; + public const int LeaveRaidCsReq = 2216; + public const int LeaveRaidScRsp = 2297; + public const int LeaveRogueCsReq = 1871; + public const int LeaveRogueScRsp = 1834; + public const int LeaveTrialActivityCsReq = 2628; + public const int LeaveTrialActivityScRsp = 2633; + public const int LockEquipmentCsReq = 504; + public const int LockEquipmentScRsp = 519; + public const int LockRelicCsReq = 539; + public const int LockRelicScRsp = 526; + public const int LogisticsDetonateStarSkiffCsReq = 4705; + public const int LogisticsDetonateStarSkiffScRsp = 4743; + public const int LogisticsGameCsReq = 4716; + public const int LogisticsGameScRsp = 4797; + public const int LogisticsInfoScNotify = 4795; + public const int LogisticsScoreRewardSyncInfoScNotify = 4708; + public const int MarkChatEmojiCsReq = 3973; + public const int MarkChatEmojiScRsp = 3906; + public const int MarkItemCsReq = 555; + public const int MarkItemScRsp = 515; + public const int MarkReadMailCsReq = 816; + public const int MarkReadMailScRsp = 897; + public const int MatchBoxingClubOpponentCsReq = 4216; + public const int MatchBoxingClubOpponentScRsp = 4297; + public const int MissionAcceptScNotify = 1255; + public const int MissionEventRewardScNotify = 1239; + public const int MissionGroupWarnScNotify = 1206; + public const int MissionRewardScNotify = 1204; + public const int MonthCardRewardNotify = 42; + public const int MultipleDropInfoNotify = 4619; + public const int MultipleDropInfoScNotify = 4616; + public const int MuseumDispatchFinishedScNotify = 4353; + public const int MuseumFundsChangedScNotify = 4326; + public const int MuseumInfoChangedScNotify = 4339; + public const int MuseumRandomEventQueryCsReq = 4360; + public const int MuseumRandomEventQueryScRsp = 4350; + public const int MuseumRandomEventSelectCsReq = 4365; + public const int MuseumRandomEventSelectScRsp = 4351; + public const int MuseumRandomEventStartScNotify = 4398; + public const int MuseumTakeCollectRewardCsReq = 4395; + public const int MuseumTakeCollectRewardScRsp = 4379; + public const int MuseumTargetMissionFinishNotify = 4312; + public const int MuseumTargetRewardNotify = 4385; + public const int MuseumTargetStartNotify = 4376; + public const int NewAssistHistoryNotify = 2905; + public const int NewMailScNotify = 900; + public const int OpenRogueChestCsReq = 1842; + public const int OpenRogueChestScRsp = 1827; + public const int OpenTreasureDungeonGridCsReq = 4481; + public const int OpenTreasureDungeonGridScRsp = 4418; + public const int PickRogueAvatarCsReq = 1818; + public const int PickRogueAvatarScRsp = 1839; + public const int PlayBackGroundMusicCsReq = 3116; + public const int PlayBackGroundMusicScRsp = 3197; + public const int PlayerGetTokenCsReq = 4; + public const int PlayerGetTokenScRsp = 19; + public const int PlayerHeartBeatCsReq = 35; + public const int PlayerHeartBeatScRsp = 70; + public const int PlayerKickOutScNotify = 100; + public const int PlayerLoginCsReq = 7; + public const int PlayerLoginFinishCsReq = 58; + public const int PlayerLoginFinishScRsp = 22; + public const int PlayerLoginScRsp = 57; + public const int PlayerLogoutCsReq = 16; + public const int PlayerLogoutScRsp = 97; + public const int PlayerReturnForceFinishScNotify = 4573; + public const int PlayerReturnInfoQueryCsReq = 4600; + public const int PlayerReturnInfoQueryScRsp = 4546; + public const int PlayerReturnPointChangeScNotify = 4597; + public const int PlayerReturnSignCsReq = 4557; + public const int PlayerReturnSignScRsp = 4516; + public const int PlayerReturnStartScNotify = 4507; + public const int PlayerReturnTakePointRewardCsReq = 4504; + public const int PlayerReturnTakePointRewardScRsp = 4519; + public const int PlayerReturnTakeRewardCsReq = 4571; + public const int PlayerReturnTakeRewardScRsp = 4534; + public const int PlayerSyncScNotify = 607; + public const int PrepareRogueAdventureRoomCsReq = 5657; + public const int PrepareRogueAdventureRoomScRsp = 5616; + public const int PrestigeLevelUpCsReq = 4750; + public const int PrestigeLevelUpScRsp = 4765; + public const int PrivateMsgOfflineUsersScNotify = 3997; + public const int PromoteAvatarCsReq = 371; + public const int PromoteAvatarScRsp = 334; + public const int PromoteEquipmentCsReq = 516; + public const int PromoteEquipmentScRsp = 597; + public const int PunkLordBattleResultScNotify = 3251; + public const int PunkLordDataChangeNotify = 3254; + public const int PunkLordMonsterInfoScNotify = 3281; + public const int PunkLordMonsterKilledNotify = 3295; + public const int PunkLordRaidTimeOutScNotify = 3298; + public const int PVEBattleResultCsReq = 107; + public const int PVEBattleResultScRsp = 157; + public const int QueryProductInfoCsReq = 90; + public const int QueryProductInfoScRsp = 38; + public const int QuestRecordScNotify = 1000; + public const int QuitBattleCsReq = 116; + public const int QuitBattleScNotify = 200; + public const int QuitBattleScRsp = 197; + public const int QuitLineupCsReq = 771; + public const int QuitLineupScRsp = 734; + public const int QuitRogueCsReq = 1824; + public const int QuitRogueScRsp = 1817; + public const int QuitTreasureDungeonCsReq = 4451; + public const int QuitTreasureDungeonScRsp = 4453; + public const int QuitWolfBroGameCsReq = 6571; + public const int QuitWolfBroGameScRsp = 6534; + public const int RaidCollectionDataCsReq = 6945; + public const int RaidCollectionDataScNotify = 6960; + public const int RaidCollectionDataScRsp = 6941; + public const int RaidInfoNotify = 2204; + public const int RaidKickByServerScNotify = 2260; + public const int RankUpAvatarCsReq = 389; + public const int RankUpAvatarScRsp = 381; + public const int RankUpEquipmentCsReq = 600; + public const int RankUpEquipmentScRsp = 546; + public const int RechargeSuccNotify = 550; + public const int RecoverAllLineupCsReq = 1417; + public const int RecoverAllLineupScRsp = 1494; + public const int ReEnterLastElementStageCsReq = 1421; + public const int ReEnterLastElementStageScRsp = 1448; + public const int RefreshAlleyOrderCsReq = 4739; + public const int RefreshAlleyOrderScRsp = 4726; + public const int RefreshTriggerByClientCsReq = 1456; + public const int RefreshTriggerByClientScNotify = 1449; + public const int RefreshTriggerByClientScRsp = 1468; + public const int RegionStopScNotify = 26; + public const int RelicRecommendCsReq = 538; + public const int RelicRecommendScRsp = 530; + public const int RemoveStuffFromAreaCsReq = 4371; + public const int RemoveStuffFromAreaScRsp = 4334; + public const int ReplaceLineupCsReq = 751; + public const int ReplaceLineupScRsp = 753; + public const int ReportPlayerCsReq = 2951; + public const int ReportPlayerScRsp = 2953; + public const int ReserveStaminaExchangeCsReq = 20; + public const int ReserveStaminaExchangeScRsp = 63; + public const int ResetMapRotationRegionCsReq = 6823; + public const int ResetMapRotationRegionScRsp = 6889; + public const int RestoreWolfBroGameArchiveCsReq = 6504; + public const int RestoreWolfBroGameArchiveScRsp = 6519; + public const int RetcodeNotify = 27; + public const int ReturnLastTownCsReq = 1450; + public const int ReturnLastTownScRsp = 1465; + public const int RevcMsgScNotify = 3916; + public const int ReviveRogueAvatarCsReq = 1898; + public const int ReviveRogueAvatarScRsp = 1860; + public const int RogueEndlessActivityBattleEndScNotify = 6004; + public const int RogueModifierAddNotify = 5316; + public const int RogueModifierDelNotify = 5400; + public const int RogueModifierSelectCellCsReq = 5397; + public const int RogueModifierSelectCellScRsp = 5304; + public const int RogueModifierStageStartNotify = 5346; + public const int RogueModifierUpdateNotify = 5334; + public const int RogueNpcDisappearCsReq = 5606; + public const int RogueNpcDisappearScRsp = 5623; + public const int RotateMapCsReq = 6871; + public const int RotateMapScRsp = 6834; + public const int SaveLogisticsCsReq = 4712; + public const int SaveLogisticsScRsp = 4785; + public const int SavePointsInfoNotify = 1455; + public const int SceneCastSkillCsReq = 1404; + public const int SceneCastSkillMpUpdateScNotify = 1418; + public const int SceneCastSkillScRsp = 1419; + public const int SceneEnterStageCsReq = 1451; + public const int SceneEnterStageScRsp = 1453; + public const int SceneEntityDieScNotify = 1413; + public const int SceneEntityDisappearScNotify = 1446; + public const int SceneEntityMoveCsReq = 1407; + public const int SceneEntityMoveScNotify = 1473; + public const int SceneEntityMoveScRsp = 1457; + public const int SceneEntityTeleportCsReq = 1462; + public const int SceneEntityTeleportScRsp = 1458; + public const int SceneEntityUpdateScNotify = 1500; + public const int SceneGroupRefreshScNotify = 1483; + public const int ScenePlaneEventScNotify = 1499; + public const int SceneUpdatePositionVersionNotify = 1406; + public const int SearchPlayerCsReq = 2985; + public const int SearchPlayerScRsp = 2995; + public const int SecurityReportCsReq = 4173; + public const int SecurityReportScRsp = 4106; + public const int SelectChatBubbleCsReq = 5116; + public const int SelectChatBubbleScRsp = 5197; + public const int SelectChessRogueNousSubStoryCsReq = 5412; + public const int SelectChessRogueNousSubStoryScRsp = 5544; + public const int SelectChessRogueSubStoryCsReq = 5471; + public const int SelectChessRogueSubStoryScRsp = 5489; + public const int SelectInclinationTextCsReq = 2200; + public const int SelectInclinationTextScRsp = 2146; + public const int SelectPhoneThemeCsReq = 5119; + public const int SelectPhoneThemeScRsp = 5171; + public const int SelectRogueDialogueEventCsReq = 1858; + public const int SelectRogueDialogueEventScRsp = 1822; + public const int SellItemCsReq = 598; + public const int SellItemScRsp = 560; + public const int SendMsgCsReq = 3907; + public const int SendMsgScRsp = 3957; + public const int ServerAnnounceNotify = 10; + public const int ServerSimulateBattleFinishScNotify = 106; + public const int SetAetherDivideLineUpCsReq = 4823; + public const int SetAetherDivideLineUpScRsp = 4889; + public const int SetAssistAvatarCsReq = 2806; + public const int SetAssistAvatarScRsp = 2823; + public const int SetAssistCsReq = 2954; + public const int SetAssistScRsp = 2924; + public const int SetBoxingClubResonanceLineupCsReq = 4223; + public const int SetBoxingClubResonanceLineupScRsp = 4289; + public const int SetClientPausedCsReq = 1411; + public const int SetClientPausedScRsp = 1402; + public const int SetClientRaidTargetCountCsReq = 2223; + public const int SetClientRaidTargetCountScRsp = 2289; + public const int SetCurInteractEntityCsReq = 1454; + public const int SetCurInteractEntityScRsp = 1424; + public const int SetCurWaypointCsReq = 416; + public const int SetCurWaypointScRsp = 497; + public const int SetDisplayAvatarCsReq = 2804; + public const int SetDisplayAvatarScRsp = 2819; + public const int SetForbidOtherApplyFriendCsReq = 2930; + public const int SetForbidOtherApplyFriendScRsp = 2975; + public const int SetFriendMarkCsReq = 2988; + public const int SetFriendMarkScRsp = 2937; + public const int SetFriendRemarkNameCsReq = 2950; + public const int SetFriendRemarkNameScRsp = 2965; + public const int SetGameplayBirthdayCsReq = 36; + public const int SetGameplayBirthdayScRsp = 72; + public const int SetGenderCsReq = 43; + public const int SetGenderScRsp = 8; + public const int SetGroupCustomSaveDataCsReq = 1470; + public const int SetGroupCustomSaveDataScRsp = 1445; + public const int SetHeadIconCsReq = 2816; + public const int SetHeadIconScRsp = 2897; + public const int SetHeroBasicTypeCsReq = 54; + public const int SetHeroBasicTypeScRsp = 24; + public const int SetIsDisplayAvatarInfoCsReq = 2871; + public const int SetIsDisplayAvatarInfoScRsp = 2834; + public const int SetLanguageCsReq = 95; + public const int SetLanguageScRsp = 79; + public const int SetLineupNameCsReq = 726; + public const int SetLineupNameScRsp = 798; + public const int SetMissionEventProgressCsReq = 1253; + public const int SetMissionEventProgressScRsp = 1276; + public const int SetNicknameCsReq = 60; + public const int SetNicknameScRsp = 50; + public const int SetPlayerInfoCsReq = 11; + public const int SetPlayerInfoScRsp = 2; + public const int SetRedPointStatusScNotify = 99; + public const int SetSignatureCsReq = 2846; + public const int SetSignatureScRsp = 2873; + public const int SetSpringRecoverConfigCsReq = 1480; + public const int SetSpringRecoverConfigScRsp = 1436; + public const int SetStuffToAreaCsReq = 4304; + public const int SetStuffToAreaScRsp = 4319; + public const int SetTurnFoodSwitchCsReq = 508; + public const int SetTurnFoodSwitchScRsp = 511; + public const int ShareCsReq = 4107; + public const int SharePunkLordMonsterCsReq = 3204; + public const int SharePunkLordMonsterScRsp = 3219; + public const int ShareScRsp = 4157; + public const int ShowNewSupplementVisitorCsReq = 3773; + public const int ShowNewSupplementVisitorScRsp = 3706; + public const int SpaceZooBornCsReq = 6716; + public const int SpaceZooBornScRsp = 6797; + public const int SpaceZooCatUpdateNotify = 6773; + public const int SpaceZooDataCsReq = 6707; + public const int SpaceZooDataScRsp = 6757; + public const int SpaceZooDeleteCatCsReq = 6800; + public const int SpaceZooDeleteCatScRsp = 6746; + public const int SpaceZooExchangeItemCsReq = 6706; + public const int SpaceZooExchangeItemScRsp = 6723; + public const int SpaceZooMutateCsReq = 6704; + public const int SpaceZooMutateScRsp = 6719; + public const int SpaceZooOpCatteryCsReq = 6771; + public const int SpaceZooOpCatteryScRsp = 6734; + public const int SpaceZooTakeCsReq = 6789; + public const int SpaceZooTakeScRsp = 6781; + public const int SpringRecoverCsReq = 1472; + public const int SpringRecoverScRsp = 1478; + public const int SpringRecoverSingleAvatarCsReq = 1442; + public const int SpringRecoverSingleAvatarScRsp = 1427; + public const int SpringRefreshCsReq = 1426; + public const int SpringRefreshScRsp = 1498; + public const int StaminaInfoScNotify = 66; + public const int StartAetherDivideChallengeBattleCsReq = 4871; + public const int StartAetherDivideChallengeBattleScRsp = 4834; + public const int StartAetherDivideSceneBattleCsReq = 4804; + public const int StartAetherDivideSceneBattleScRsp = 4819; + public const int StartAetherDivideStageBattleCsReq = 4850; + public const int StartAetherDivideStageBattleScRsp = 4865; + public const int StartAlleyEventCsReq = 4771; + public const int StartAlleyEventScRsp = 4734; + public const int StartBattleCollegeCsReq = 5797; + public const int StartBattleCollegeScRsp = 5704; + public const int StartBoxingClubBattleCsReq = 4204; + public const int StartBoxingClubBattleScRsp = 4219; + public const int StartChallengeCsReq = 1716; + public const int StartChallengeScRsp = 1797; + public const int StartCocoonStageCsReq = 1415; + public const int StartCocoonStageScRsp = 1405; + public const int StartFinishMainMissionScNotify = 1210; + public const int StartFinishSubMissionScNotify = 1279; + public const int StartPunkLordRaidCsReq = 3216; + public const int StartPunkLordRaidScRsp = 3297; + public const int StartRaidCsReq = 2207; + public const int StartRaidScRsp = 2257; + public const int StartRogueCsReq = 1816; + public const int StartRogueScRsp = 1897; + public const int StartTimedCocoonStageCsReq = 1431; + public const int StartTimedCocoonStageScRsp = 1464; + public const int StartTimedFarmElementCsReq = 1440; + public const int StartTimedFarmElementScRsp = 1441; + public const int StartTrialActivityCsReq = 2670; + public const int StartTrialActivityScRsp = 2645; + public const int StartWolfBroGameCsReq = 6507; + public const int StartWolfBroGameScRsp = 6557; + public const int StopRogueAdventureRoomCsReq = 5676; + public const int StopRogueAdventureRoomScRsp = 5612; + public const int StoryLineInfoScNotify = 6216; + public const int StoryLineTrialAvatarChangeScNotify = 6271; + public const int StrongChallengeActivityBattleEndScNotify = 6604; + public const int SubMissionRewardScNotify = 1212; + public const int SubmitEmotionItemCsReq = 6304; + public const int SubmitEmotionItemScRsp = 6319; + public const int SubmitMonsterResearchActivityMaterialCsReq = 2698; + public const int SubmitMonsterResearchActivityMaterialScRsp = 2660; + public const int SubmitOfferingItemCsReq = 6940; + public const int SubmitOfferingItemScRsp = 6922; + public const int SubmitOrigamiItemCsReq = 4181; + public const int SubmitOrigamiItemScRsp = 4118; + public const int SummonPunkLordMonsterCsReq = 3271; + public const int SummonPunkLordMonsterScRsp = 3234; + public const int SwapLineupCsReq = 800; + public const int SwapLineupScRsp = 746; + public const int SwitchAetherDivideLineUpSlotCsReq = 4898; + public const int SwitchAetherDivideLineUpSlotScRsp = 4860; + public const int SwitchLineupIndexCsReq = 718; + public const int SwitchLineupIndexScRsp = 739; + public const int SyncAcceptedPamMissionNotify = 4016; + public const int SyncAddBlacklistScNotify = 2926; + public const int SyncApplyFriendScNotify = 3000; + public const int SyncChessRogueMainStoryFinishScNotify = 5441; + public const int SyncChessRogueNousMainStoryScNotify = 5514; + public const int SyncChessRogueNousSubStoryScNotify = 5444; + public const int SyncChessRogueNousValueScNotify = 5484; + public const int SyncClientResVersionCsReq = 171; + public const int SyncClientResVersionScRsp = 134; + public const int SyncDeleteFriendScNotify = 2981; + public const int SyncEntityBuffChangeListScNotify = 1423; + public const int SyncHandleFriendScNotify = 2906; + public const int SyncLineupNotify = 773; + public const int SyncRogueAdventureRoomInfoScNotify = 5607; + public const int SyncRogueAeonLevelUpRewardScNotify = 1874; + public const int SyncRogueAeonScNotify = 1892; + public const int SyncRogueAreaUnlockScNotify = 1899; + public const int SyncRogueCommonActionResultScNotify = 5638; + public const int SyncRogueCommonPendingActionScNotify = 5630; + public const int SyncRogueCommonVirtualItemInfoScNotify = 5637; + public const int SyncRogueDialogueEventDataScNotify = 1893; + public const int SyncRogueExploreWinScNotify = 1855; + public const int SyncRogueFinishScNotify = 1881; + public const int SyncRogueGetItemScNotify = 1809; + public const int SyncRogueHandbookDataUpdateScNotify = 5608; + public const int SyncRogueMapRoomScNotify = 1890; + public const int SyncRoguePickAvatarInfoScNotify = 1884; + public const int SyncRogueReviveInfoScNotify = 1854; + public const int SyncRogueRewardInfoScNotify = 1832; + public const int SyncRogueSeasonFinishScNotify = 1815; + public const int SyncRogueStatusScNotify = 1841; + public const int SyncRogueVirtualItemInfoScNotify = 1840; + public const int SyncServerSceneChangeNotify = 1420; + public const int SyncTaskCsReq = 1219; + public const int SyncTaskScRsp = 1271; + public const int SyncTurnFoodNotify = 543; + public const int TakeActivityExpeditionRewardCsReq = 2589; + public const int TakeActivityExpeditionRewardScRsp = 2581; + public const int TakeAllApRewardCsReq = 3319; + public const int TakeAllApRewardScRsp = 3371; + public const int TakeAllRewardCsReq = 3034; + public const int TakeAllRewardScRsp = 3100; + public const int TakeApRewardCsReq = 3307; + public const int TakeApRewardScRsp = 3357; + public const int TakeAssistRewardCsReq = 2943; + public const int TakeAssistRewardScRsp = 2908; + public const int TakeBpRewardCsReq = 3097; + public const int TakeBpRewardScRsp = 3004; + public const int TakeChallengeRaidRewardCsReq = 2234; + public const int TakeChallengeRaidRewardScRsp = 2300; + public const int TakeChallengeRewardCsReq = 1781; + public const int TakeChallengeRewardScRsp = 1718; + public const int TakeChapterRewardCsReq = 434; + public const int TakeChapterRewardScRsp = 500; + public const int TakeCityShopRewardCsReq = 1504; + public const int TakeCityShopRewardScRsp = 1519; + public const int TakeExpeditionRewardCsReq = 2571; + public const int TakeExpeditionRewardScRsp = 2534; + public const int TakeFightActivityRewardCsReq = 3619; + public const int TakeFightActivityRewardScRsp = 3671; + public const int TakeKilledPunkLordMonsterScoreCsReq = 3279; + public const int TakeKilledPunkLordMonsterScoreScRsp = 3210; + public const int TakeLoginActivityRewardCsReq = 2616; + public const int TakeLoginActivityRewardScRsp = 2697; + public const int TakeMailAttachmentCsReq = 871; + public const int TakeMailAttachmentScRsp = 834; + public const int TakeMonsterResearchActivityRewardCsReq = 2650; + public const int TakeMonsterResearchActivityRewardScRsp = 2665; + public const int TakeOffAvatarSkinCsReq = 353; + public const int TakeOffAvatarSkinScRsp = 376; + public const int TakeOffEquipmentCsReq = 373; + public const int TakeOffEquipmentScRsp = 306; + public const int TakeOfferingRewardCsReq = 6923; + public const int TakeOfferingRewardScRsp = 6932; + public const int TakeOffRelicCsReq = 326; + public const int TakeOffRelicScRsp = 398; + public const int TakePictureCsReq = 4104; + public const int TakePictureScRsp = 4119; + public const int TakePrestigeRewardCsReq = 4773; + public const int TakePrestigeRewardScRsp = 4706; + public const int TakePromotionRewardCsReq = 360; + public const int TakePromotionRewardScRsp = 350; + public const int TakePunkLordPointRewardCsReq = 3223; + public const int TakePunkLordPointRewardScRsp = 3289; + public const int TakeQuestOptionalRewardCsReq = 906; + public const int TakeQuestOptionalRewardScRsp = 923; + public const int TakeQuestRewardCsReq = 916; + public const int TakeQuestRewardScRsp = 997; + public const int TakeRogueAeonLevelRewardCsReq = 1829; + public const int TakeRogueAeonLevelRewardScRsp = 1820; + public const int TakeRogueEndlessActivityAllBonusRewardCsReq = 6034; + public const int TakeRogueEndlessActivityAllBonusRewardScRsp = 6100; + public const int TakeRogueEndlessActivityPointRewardCsReq = 6019; + public const int TakeRogueEndlessActivityPointRewardScRsp = 6071; + public const int TakeRogueEventHandbookRewardCsReq = 5613; + public const int TakeRogueEventHandbookRewardScRsp = 5690; + public const int TakeRogueMiracleHandbookRewardCsReq = 5611; + public const int TakeRogueMiracleHandbookRewardScRsp = 5602; + public const int TakeRogueScoreRewardCsReq = 1850; + public const int TakeRogueScoreRewardScRsp = 1865; + public const int TakeRollShopRewardCsReq = 6903; + public const int TakeRollShopRewardScRsp = 6912; + public const int TakeTalkRewardCsReq = 2116; + public const int TakeTalkRewardScRsp = 2197; + public const int TakeTrainVisitorUntakenBehaviorRewardCsReq = 3800; + public const int TakeTrainVisitorUntakenBehaviorRewardScRsp = 3746; + public const int TakeTrialActivityRewardCsReq = 2627; + public const int TakeTrialActivityRewardScRsp = 2635; + public const int TeleportToMissionResetPointCsReq = 1285; + public const int TeleportToMissionResetPointScRsp = 1295; + public const int TelevisionActivityBattleEndScNotify = 6972; + public const int TelevisionActivityDataChangeScNotify = 6980; + public const int TextJoinBatchSaveCsReq = 3804; + public const int TextJoinBatchSaveScRsp = 3819; + public const int TextJoinQueryCsReq = 3816; + public const int TextJoinQueryScRsp = 3897; + public const int TextJoinSaveCsReq = 3807; + public const int TextJoinSaveScRsp = 3857; + public const int TrainRefreshTimeNotify = 3704; + public const int TrainVisitorBehaviorFinishCsReq = 3707; + public const int TrainVisitorBehaviorFinishScRsp = 3757; + public const int TrainVisitorRewardSendNotify = 3719; + public const int TravelBrochureApplyPasterCsReq = 6471; + public const int TravelBrochureApplyPasterListCsReq = 6450; + public const int TravelBrochureApplyPasterListScRsp = 6465; + public const int TravelBrochureApplyPasterScRsp = 6434; + public const int TravelBrochureGetDataCsReq = 6407; + public const int TravelBrochureGetDataScRsp = 6457; + public const int TravelBrochureGetPasterScNotify = 6423; + public const int TravelBrochurePageResetCsReq = 6498; + public const int TravelBrochurePageResetScRsp = 6460; + public const int TravelBrochurePageUnlockScNotify = 6416; + public const int TravelBrochureRemovePasterCsReq = 6500; + public const int TravelBrochureRemovePasterScRsp = 6446; + public const int TravelBrochureSelectMessageCsReq = 6404; + public const int TravelBrochureSelectMessageScRsp = 6419; + public const int TravelBrochureSetCustomValueCsReq = 6481; + public const int TravelBrochureSetCustomValueScRsp = 6418; + public const int TravelBrochureSetPageDescStatusCsReq = 6439; + public const int TravelBrochureSetPageDescStatusScRsp = 6426; + public const int TravelBrochureUpdatePasterPosCsReq = 6473; + public const int TravelBrochureUpdatePasterPosScRsp = 6406; + public const int TreasureDungeonDataScNotify = 4407; + public const int TreasureDungeonFinishScNotify = 4457; + public const int TrialActivityDataChangeScNotify = 2678; + public const int TrialBackGroundMusicCsReq = 3171; + public const int TrialBackGroundMusicScRsp = 3134; + public const int TriggerVoiceCsReq = 4123; + public const int TriggerVoiceScRsp = 4189; + public const int UnlockAvatarSkinScNotify = 312; + public const int UnlockBackGroundMusicCsReq = 3104; + public const int UnlockBackGroundMusicScRsp = 3119; + public const int UnlockChatBubbleScNotify = 5104; + public const int UnlockedAreaMapScNotify = 1477; + public const int UnlockHeadIconScNotify = 2900; + public const int UnlockPhoneThemeScNotify = 5134; + public const int UnlockSkilltreeCsReq = 304; + public const int UnlockSkilltreeScRsp = 319; + public const int UnlockTeleportNotify = 1432; + public const int UnlockTutorialCsReq = 1604; + public const int UnlockTutorialGuideCsReq = 1671; + public const int UnlockTutorialGuideScRsp = 1634; + public const int UnlockTutorialScRsp = 1619; + public const int UpdateEnergyScNotify = 6818; + public const int UpdateFeatureSwitchScNotify = 75; + public const int UpdateFloorSavedValueNotify = 1474; + public const int UpdateMapRotationDataScNotify = 6839; + public const int UpdateMechanismBarScNotify = 1435; + public const int UpdatePlayerSettingCsReq = 67; + public const int UpdatePlayerSettingScRsp = 74; + public const int UpdateRedDotDataCsReq = 5916; + public const int UpdateRedDotDataScRsp = 5997; + public const int UpdateRogueAdventureRoomScoreCsReq = 5675; + public const int UpdateRogueAdventureRoomScoreScRsp = 5688; + public const int UpdateServerPrefsDataCsReq = 6104; + public const int UpdateServerPrefsDataScRsp = 6119; + public const int UpdateTrackMainMissionIdCsReq = 1205; + public const int UpdateTrackMainMissionIdScRsp = 1243; + public const int UpgradeAreaCsReq = 4323; + public const int UpgradeAreaScRsp = 4389; + public const int UpgradeAreaStatCsReq = 4381; + public const int UpgradeAreaStatScRsp = 4318; + public const int UseItemCsReq = 571; + public const int UseItemScRsp = 534; + public const int UseTreasureDungeonItemCsReq = 4450; + public const int UseTreasureDungeonItemScRsp = 4465; + public const int VirtualLineupDestroyNotify = 765; + public const int WaypointShowNewCsNotify = 471; + public const int WolfBroGameDataChangeScNotify = 6573; + } +} diff --git a/GameServer/Server/Packet/Handler.cs b/GameServer/Server/Packet/Handler.cs new file mode 100644 index 00000000..3d1358eb --- /dev/null +++ b/GameServer/Server/Packet/Handler.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet +{ + public abstract class Handler + { + public abstract void OnHandle(byte[] header, byte[] data); + } +} diff --git a/GameServer/Server/Packet/HandlerManager.cs b/GameServer/Server/Packet/HandlerManager.cs new file mode 100644 index 00000000..8e10f5a9 --- /dev/null +++ b/GameServer/Server/Packet/HandlerManager.cs @@ -0,0 +1,40 @@ +using EggLink.DanhengServer.Data; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet +{ + public class HandlerManager + { + public Dictionary handlers = []; + + public HandlerManager() + { + var classes = Assembly.GetExecutingAssembly().GetTypes(); // Get all classes in the assembly + foreach (var cls in classes) + { + var attribute = (Opcode)Attribute.GetCustomAttribute(cls, typeof(Opcode)); + + if (attribute != null) + { + handlers.Add(attribute.CmdId, (Handler)Activator.CreateInstance(cls)); + } + } + } + + public Handler? GetHandler(int cmdId) + { + try + { + return handlers[cmdId]; + } catch + { + return null; + } + } + } +} diff --git a/GameServer/Server/Packet/Opcode.cs b/GameServer/Server/Packet/Opcode.cs new file mode 100644 index 00000000..5ee0b0f8 --- /dev/null +++ b/GameServer/Server/Packet/Opcode.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet +{ + [AttributeUsage(AttributeTargets.Class)] + public class Opcode(int cmdId) : Attribute + { + public int CmdId = cmdId; + } +} diff --git a/GameServer/Server/Packet/Recv/Player/HandlerPlayerGetTokenCsReq.cs b/GameServer/Server/Packet/Recv/Player/HandlerPlayerGetTokenCsReq.cs new file mode 100644 index 00000000..15ccb9cd --- /dev/null +++ b/GameServer/Server/Packet/Recv/Player/HandlerPlayerGetTokenCsReq.cs @@ -0,0 +1,20 @@ +using EggLink.DanhengServer.Proto; +using EggLink.DanhengServer.Util; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace EggLink.DanhengServer.Server.Packet.Recv.Player +{ + [Opcode(CmdId.PlayerGetTokenCsReq)] + public class HandlerPlayerGetTokenCsReq : Handler + { + public override void OnHandle(byte[] header, byte[] data) + { + var req = PlayerGetTokenCsReq.Parser.ParseFrom(data); + Logger.GetByClassName().Debug("OnHandle" + req.ToString()); + } + } +} diff --git a/WebServer/Handler/QueryGatewayHandler.cs b/WebServer/Handler/QueryGatewayHandler.cs index 08c136e6..fc0c1c0d 100644 --- a/WebServer/Handler/QueryGatewayHandler.cs +++ b/WebServer/Handler/QueryGatewayHandler.cs @@ -17,13 +17,13 @@ namespace EggLink.DanhengServer.Server.Http.Handler var gateServer = new Gateserver() { RegionName = config.GameServer.GameServerId, Ip = config.GameServer.PublicAddress, - Port = (uint)config.GameServer.PublicPort, + Port = config.GameServer.PublicPort, Msg = "Access verification failed. Please check if you have logged in to the correct account and server.", Unk1 = true, Unk2 = true, Unk3 = true, Unk4 = true, - Unk5 = true + Unk5 = true, }; if (urlData.AssetBundleUrl != null) @@ -39,13 +39,13 @@ namespace EggLink.DanhengServer.Server.Http.Handler if (urlData.LuaUrl != null) { gateServer.LuaUrl = urlData.LuaUrl; - gateServer.MdkResVersion = urlData.LuaUrl.Split('/')[urlData.LuaUrl.Split('/').Length - 1].Split('_')[1]; + gateServer.MdkResVersion = urlData.LuaUrl.Split('/')[^1].Split('_')[1]; } if (urlData.IfixUrl != null) { gateServer.IfixUrl = urlData.IfixUrl; - gateServer.IfixVersion = urlData.IfixUrl.Split('/')[urlData.IfixUrl.Split('/').Length - 1].Split('_')[1]; + gateServer.IfixVersion = urlData.IfixUrl.Split('/')[^1].Split('_')[1]; } Logger.Info("Client request: query_gateway");