feat: Upgrade server with HyNB v2.4 modular architecture, minigames, admin GM web panel, companion summons, and car radio

This commit is contained in:
CAtlas0784 committed 2026-10-29 02:44:24 +07:00
1 parent 74e2963a12
commit 3e5adf1dc5
1571 files changed
+336468 -6348

No files matched your search

+76 -19
View File
@@ -8,9 +8,20 @@ namespace Ananta.SDK.Network;
public sealed class TcpSession
{
private readonly SemaphoreSlim _sendLock = new(1, 1);
private readonly CancellationTokenSource _closed = new();
/// <summary>会话关闭时触发。逐字对齐捐赠端 <c>TcpSession.ClosedToken</c>(<c>Ananta.SDK/Network/TcpSession.cs:13</c>)。
/// ⛑ 主线原本已有 <c>_closed</c> 且 <c>Close()</c> 会 Cancel,只是没公开 → 这里<b>只加属性,不改生命周期</b>。</summary>
public CancellationToken ClosedToken => _closed.Token;
private Task? _heartbeat;
private int _isClosed;
private UxChaCha8? _readCryptor;
private UxChaCha8? _writeCryptor;
/// <summary>服务端 → 客户端传输层 keepalive 间隔(毫秒)。<b>0 = 关闭</b>(沿用旧行为)。</summary>
public int HeartbeatMs { get; }
/// <summary>单次加密写的上限(毫秒);超时会退役这条连接。<b>0 = 不设上限</b>(沿用旧行为)。</summary>
public int WriteTimeoutMs { get; }
public string Id { get; } = Guid.NewGuid().ToString("N")[..6];
public TcpClient Client { get; }
public NetworkStream Stream { get; }
@@ -18,11 +29,15 @@ public sealed class TcpSession
public Dictionary<string, object> Items { get; } = new();
public bool CryptReady => _readCryptor is not null && _writeCryptor is not null;
public TcpSession(TcpClient client, string scope)
public TcpSession(TcpClient client, string scope, int heartbeatMs = 0, int writeTimeoutMs = 15000)
{
Client = client;
Stream = client.GetStream();
Log = new ServerLogger($"{scope}:{Id}");
HeartbeatMs = heartbeatMs;
WriteTimeoutMs = writeTimeoutMs;
// Small real-time RPCs should not wait for Nagle/delayed-ACK batching.
Client.NoDelay = true;
}
public Task<Frame?> ReadFrameAsync(CancellationToken token)
@@ -69,23 +84,36 @@ public sealed class TcpSession
// UXRSA.Verify result branch is patched out to accept the local
// unsigned handshake.
await _sendLock.WaitAsync(token);
try
{
// The handshake reply itself is plaintext. Encryption becomes active
// only for the next frame in each direction.
await FrameCodec.WriteFrameAsync(Stream, 0x01, payload, cryptor: null, token: token);
_readCryptor = new UxChaCha8(clientWriteKey, streamNonce);
_writeCryptor = new UxChaCha8(clientReadKey, streamNonce);
}
finally
{
_sendLock.Release();
}
// The handshake reply itself is plaintext. Encryption becomes active
// only for the next frame in each direction — which is why the keys are
// published *after* this write instead of before it.
await WriteRawAsync(0x01, payload, token);
_readCryptor = new UxChaCha8(clientWriteKey, streamNonce);
_writeCryptor = new UxChaCha8(clientReadKey, streamNonce);
Log.Info($"HS keys clientWrite={Convert.ToHexString(clientWriteKey)} clientRead={Convert.ToHexString(clientReadKey)} nonce={Convert.ToHexString(streamNonce)} aes={Convert.ToHexString(aesKey)}");
Log.Info("HS ok -> UXChaCha8 enabled");
if (HeartbeatMs > 0) _heartbeat ??= SendHeartbeatsAsync();
}
private async Task SendHeartbeatsAsync()
{
// Keep an inbound liveness signal on every socket, including idle gates,
// independently of the client's RPC and heartbeat scheduling.
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(HeartbeatMs));
try
{
while (await timer.WaitForNextTickAsync(_closed.Token))
await SendFrameAsync(0x03, BitConverter.GetBytes(Environment.TickCount64), _closed.Token);
}
catch (OperationCanceledException) when (_closed.IsCancellationRequested) { }
catch (Exception ex)
{
Log.Warn($"heartbeat send stopped: {ex.Message}");
Close();
}
}
public Task SendFrameAsync(byte mode, byte[] body, CancellationToken token)
@@ -104,22 +132,51 @@ public sealed class TcpSession
return WriteEncryptedFrameAsync(0x09, RpcPacketWriter.WriteReturn(methodId, invokeId, err, body), token);
}
private async Task WriteEncryptedFrameAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
private Task WriteRawAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
=> WriteFrameAsync(mode, body, null, token);
private async Task WriteFrameAsync(byte mode, ReadOnlyMemory<byte> body, UxChaCha8? cryptor, CancellationToken token)
{
await _sendLock.WaitAsync(token);
using var deadline = WriteTimeoutMs > 0
? CancellationTokenSource.CreateLinkedTokenSource(token, _closed.Token)
: null;
if (deadline is not null) deadline.CancelAfter(WriteTimeoutMs);
var linked = deadline?.Token ?? token;
var acquired = false;
try
{
await FrameCodec.WriteFrameAsync(Stream, mode, body, _writeCryptor, token);
await _sendLock.WaitAsync(linked);
acquired = true;
await FrameCodec.WriteFrameAsync(Stream, mode, body, cryptor, linked);
}
catch (OperationCanceledException) when (!_closed.IsCancellationRequested && !token.IsCancellationRequested)
{
// A partially-written encrypted frame cannot be retried: the stream cipher already
// advanced. Retire only this socket so everyone else keeps being served.
Close();
throw new IOException($"TCP send cancelled or exceeded the {(WriteTimeoutMs > 0 ? WriteTimeoutMs : 0)}-ms write deadline");
}
catch (Exception ex) when (ex is IOException or SocketException)
{
Close();
throw;
}
finally
{
_sendLock.Release();
if (acquired) _sendLock.Release();
}
}
private Task WriteEncryptedFrameAsync(byte mode, ReadOnlyMemory<byte> body, CancellationToken token)
=> WriteFrameAsync(mode, body, _writeCryptor, token);
public void Close()
{
if (Interlocked.Exchange(ref _isClosed, 1) != 0) return;
_closed.Cancel();
try { Client.Close(); } catch { }
_sendLock.Dispose();
// Pending async sends may still own or await this semaphore. Disposing it
// here races their finally/Release and hides the actual socket closure.
// It owns no wait handle; let it be collected with this session.
}
}