初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
|
||||
public interface IRequestHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the request type this handler is responsible for, e.g., "c:ping".
|
||||
/// </summary>
|
||||
string RequestType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handle an incoming request and returns a response.
|
||||
/// </summary>
|
||||
/// <param name="requestBody">The raw body of the request.</param>
|
||||
/// <param name="context">The shared server context.</param>
|
||||
/// <param name="sessionId">A unique identifier for the client session.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A tuple containing the status code and the response body.</returns>
|
||||
Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync
|
||||
(ReadOnlyMemory<byte> requestBody, IServerContext context, string sessionId, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
|
||||
public interface IServerContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the list of currently connected player profiles.
|
||||
/// </summary>
|
||||
IReadOnlyList<PlayerProfile> PlayerProfiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of currently connected player profiles, keyed by a unique session identifier.
|
||||
/// </summary>
|
||||
ConcurrentDictionary<string, TrackedPlayerProfile> TrackedPlayers { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Occurs on player profile changed.
|
||||
/// </summary>
|
||||
void OnPlayerProfilesChanged();
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the list of player profiles changes.
|
||||
/// </summary>
|
||||
event Action<IReadOnlyList<PlayerProfile>> PlayerProfilesPing;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prot of the running Minecraft server.
|
||||
/// </summary>
|
||||
int MinecraftServerProt { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the room information.
|
||||
/// </summary>
|
||||
LobbyInfo UserLobbyInfo { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Player name(Host).
|
||||
/// </summary>
|
||||
string PlayerName { get; }
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.EasyTier;
|
||||
using PCL.Core.App;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
|
||||
public class GetPlayerProfileListHandler : IRequestHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:player_profiles_list";
|
||||
|
||||
private static readonly JsonSerializerOptions _JsonOptions = new(JsonCompat.SerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync(ReadOnlyMemory<byte> requestBody,
|
||||
IServerContext context, string sessionId, CancellationToken ct)
|
||||
{
|
||||
var hostProfile = new PlayerProfile
|
||||
{
|
||||
Name = context.PlayerName,
|
||||
MachineId = Utils.Secret.Identify.LauncherId,
|
||||
Vendor = $"PCL CE {Basics.VersionName}, EasyTier {EasyTierMetadata.CurrentEasyTierVer}",
|
||||
Kind = PlayerKind.HOST
|
||||
};
|
||||
|
||||
var allProfiles = context.PlayerProfiles;
|
||||
|
||||
var responseBody = JsonSerializer.SerializeToUtf8Bytes(allProfiles, _JsonOptions);
|
||||
|
||||
return Task.FromResult(((byte)0, new ReadOnlyMemory<byte>(responseBody)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
|
||||
public class GetProtocolsHandler : IRequestHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:protocols";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync(ReadOnlyMemory<byte> requestBody,
|
||||
IServerContext context, string sessionId, CancellationToken ct)
|
||||
{
|
||||
string[] protocols =
|
||||
[
|
||||
"c:ping",
|
||||
"c:protocols",
|
||||
"c:server_port",
|
||||
"c:player_ping",
|
||||
"c:player_profiles_list"
|
||||
];
|
||||
|
||||
var rseponseContent = string.Join('\0', protocols);
|
||||
var responseBody = Encoding.ASCII.GetBytes(rseponseContent);
|
||||
|
||||
return Task.FromResult(((byte)0, new ReadOnlyMemory<byte>(responseBody)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
|
||||
public class GetServerPortHandler : IRequestHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:server_port";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync(ReadOnlyMemory<byte> requestBody,
|
||||
IServerContext context, string sessionId, CancellationToken ct)
|
||||
{
|
||||
var port = context.MinecraftServerProt;
|
||||
|
||||
if (port == 0)
|
||||
{
|
||||
return Task.FromResult<(byte, ReadOnlyMemory<byte>)>((32, ReadOnlyMemory<byte>.Empty));
|
||||
}
|
||||
|
||||
|
||||
var portBytes = new byte[2];
|
||||
var portAsUshort = (ushort)port;
|
||||
|
||||
BinaryPrimitives.WriteUInt16BigEndian(portBytes.AsSpan(), portAsUshort);
|
||||
|
||||
return Task.FromResult<(byte, ReadOnlyMemory<byte>)>((0, portBytes));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
|
||||
public class PingHandler : IRequestHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:ping";
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync(ReadOnlyMemory<byte> requestBody,
|
||||
IServerContext context, string sessionId, CancellationToken ct)
|
||||
{
|
||||
return Task.FromResult((Status: (byte)0, Body: requestBody));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
|
||||
public class PlayerPingHandler : IRequestHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string RequestType { get; } = "c:player_ping";
|
||||
|
||||
private static readonly JsonSerializerOptions _JsonOptions = new(JsonCompat.SerializerOptions)
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<(byte Status, ReadOnlyMemory<byte> Body)> HandleAsync(ReadOnlyMemory<byte> requestBody,
|
||||
IServerContext context, string sessionId, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
var profile = JsonSerializer.Deserialize<PlayerProfile>(requestBody.Span, _JsonOptions);
|
||||
if (profile is null || string.IsNullOrEmpty(profile.MachineId))
|
||||
{
|
||||
LogWrapper.Warn("ScaffoldingServer",
|
||||
$"Received a player_ping from session {sessionId} with a missing or empty machine_id. Ignoring.");
|
||||
return Task.FromResult(((byte)32, ReadOnlyMemory<byte>.Empty));
|
||||
}
|
||||
|
||||
var guestProfile = profile with { Kind = PlayerKind.GUEST };
|
||||
|
||||
var listChanged = false;
|
||||
context.TrackedPlayers.AddOrUpdate(guestProfile.MachineId,
|
||||
_ =>
|
||||
{
|
||||
LogWrapper.Info("ScaffoldingServer",
|
||||
$"New player '{guestProfile.Name}' with machine_id '{guestProfile.MachineId}' connected.");
|
||||
var newPlayer = new TrackedPlayerProfile { Profile = profile, LastSeenUtc = DateTime.UtcNow };
|
||||
listChanged = true;
|
||||
return newPlayer;
|
||||
},
|
||||
(_, existingPlayer) =>
|
||||
{
|
||||
existingPlayer.Profile = guestProfile;
|
||||
existingPlayer.LastSeenUtc = DateTime.UtcNow;
|
||||
|
||||
return existingPlayer;
|
||||
});
|
||||
|
||||
if (listChanged)
|
||||
{
|
||||
context.OnPlayerProfilesChanged();
|
||||
}
|
||||
|
||||
return Task.FromResult(((byte)0, ReadOnlyMemory<byte>.Empty));
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
LogWrapper.Warn("ScaffoldingServer",
|
||||
$"Failed to deserialize player_ping JSON from session {sessionId}. Error: {ex.Message}");
|
||||
return Task.FromResult(((byte)32, ReadOnlyMemory<byte>.Empty));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
using PCL.Core.Link.Scaffolding.Server.Handlers;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Pipelines;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server;
|
||||
|
||||
/// <summary>
|
||||
/// A server for the Scaffolding data exchange protocol.
|
||||
/// </summary>
|
||||
public sealed class ScaffoldingServer : IAsyncDisposable
|
||||
{
|
||||
private readonly TcpListener _listener;
|
||||
private readonly IServerContext _context;
|
||||
private readonly Dictionary<string, IRequestHandler> _handlers;
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private Task? _listenTask;
|
||||
private Task? _cleanupTask;
|
||||
|
||||
private static readonly TimeSpan _PlayerTimeout = TimeSpan.FromSeconds(10);
|
||||
private static readonly TimeSpan _CleanupInterval = TimeSpan.FromSeconds(5);
|
||||
|
||||
#region Events
|
||||
|
||||
public event Action<IReadOnlyList<PlayerProfile>>? ServerStarted;
|
||||
public event Action? ServerStopped;
|
||||
public event Action<Exception?>? ServerException;
|
||||
public event Action<IReadOnlyList<PlayerProfile>>? PlayerProfilePing;
|
||||
|
||||
private void _OnContextPlayersPing(IReadOnlyList<PlayerProfile> players)
|
||||
{
|
||||
PlayerProfilePing?.Invoke(players);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public ScaffoldingServer(int port, IServerContext context)
|
||||
{
|
||||
_listener = new TcpListener(IPAddress.Loopback, port);
|
||||
_context = context;
|
||||
|
||||
_context.PlayerProfilesPing += _OnContextPlayersPing;
|
||||
|
||||
_handlers = new()
|
||||
{
|
||||
["c:player_ping"] = new PlayerPingHandler(),
|
||||
["c:server_port"] = new GetServerPortHandler(),
|
||||
["c:player_profiles_list"] = new GetPlayerProfileListHandler(),
|
||||
["c:protocols"] = new GetProtocolsHandler(),
|
||||
["c:ping"] = new PingHandler()
|
||||
};
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
try
|
||||
{
|
||||
_listener.Start();
|
||||
LogWrapper.Info("ScaffoldingServer",
|
||||
$"Successfully bound to {_listener.LocalEndpoint}. Starting to accept clients.");
|
||||
}
|
||||
catch (SocketException ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer",
|
||||
$"Failed to start TCP listener on port {((IPEndPoint)_listener.LocalEndpoint).Port}. The port might be in use or blocked.");
|
||||
ServerException?.Invoke(ex);
|
||||
return; // 启动失败,直接返回
|
||||
}
|
||||
|
||||
_listenTask = _ListenForClientsAsync(_cts.Token);
|
||||
_cleanupTask = _MonitorPlayerLivenessAsync(_cts.Token);
|
||||
|
||||
_listenTask.ContinueWith(t =>
|
||||
{
|
||||
LogWrapper.Error(t.Exception, "ScaffoldingServer",
|
||||
"The main listening task failed unexpectedly. The server is no longer accepting new connections.");
|
||||
ServerException?.Invoke(t.Exception);
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
_cleanupTask.ContinueWith(
|
||||
t =>
|
||||
{
|
||||
LogWrapper.Error(t.Exception, "ScaffoldingServer", "The player cleanup task failed unexpectedly.");
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
|
||||
LogWrapper.Debug("ScaffoldingServer", "Successfully scheduled server background tasks.");
|
||||
|
||||
ServerStarted?.Invoke(_context.PlayerProfiles);
|
||||
}
|
||||
|
||||
private async Task _MonitorPlayerLivenessAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(_CleanupInterval, ct).ConfigureAwait(false);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var timedOutPlayerKeys = new List<string>();
|
||||
|
||||
foreach (var (machineId, trackedPlayer) in _context.TrackedPlayers)
|
||||
{
|
||||
if (trackedPlayer.Profile.Kind is PlayerKind.HOST)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (now - trackedPlayer.LastSeenUtc > _PlayerTimeout)
|
||||
{
|
||||
timedOutPlayerKeys.Add(machineId);
|
||||
}
|
||||
}
|
||||
|
||||
if (timedOutPlayerKeys.Count > 0)
|
||||
{
|
||||
var listChanged = false;
|
||||
foreach (var key in timedOutPlayerKeys)
|
||||
{
|
||||
if (_context.TrackedPlayers.TryRemove(key, out var removedPlayer))
|
||||
{
|
||||
listChanged = true;
|
||||
LogWrapper.Info("ScaffoldingServer",
|
||||
$"Player '{removedPlayer.Profile.Name}' timed out and was removed.");
|
||||
}
|
||||
}
|
||||
|
||||
if (listChanged)
|
||||
{
|
||||
_context.OnPlayerProfilesChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer", "An error occurred in the player cleanup task.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _ListenForClientsAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tcpClient = await _listener.AcceptTcpClientAsync(ct).ConfigureAwait(false);
|
||||
LogWrapper.Debug("ScaffoldingServer", $"Client connected: {tcpClient.Client.RemoteEndPoint}");
|
||||
_ = _HandleClientAsync(tcpClient, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWrapper.Debug("ScaffoldingServer", "Listening task cancelled.");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer", "Occurred an exception when server running.");
|
||||
|
||||
try
|
||||
{
|
||||
_listener.Stop();
|
||||
}
|
||||
catch (Exception lisEx)
|
||||
{
|
||||
LogWrapper.Error(lisEx, "ScaffoldingServer", "Occurred an exception when stop listening port.");
|
||||
}
|
||||
|
||||
|
||||
ServerStopped?.Invoke();
|
||||
break;
|
||||
}
|
||||
|
||||
LogWrapper.Debug("ScaffoldingServer", "Listening task finished.");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task _HandleClientAsync(TcpClient tcpClient, CancellationToken ct)
|
||||
{
|
||||
var sessionId = Guid.NewGuid().ToString();
|
||||
var clientEndPoint = tcpClient.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||
LogWrapper.Debug("ScaffoldingServer", $"New connection {sessionId} from {clientEndPoint}.");
|
||||
|
||||
using (tcpClient)
|
||||
{
|
||||
var stream = tcpClient.GetStream();
|
||||
var reader = PipeReader.Create(stream);
|
||||
var writer = PipeWriter.Create(stream);
|
||||
|
||||
try
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
ReadResult readResult;
|
||||
try
|
||||
{
|
||||
readResult = await reader.ReadAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (IOException ex) when (ex.InnerException is SocketException se &&
|
||||
se.SocketErrorCode == SocketError.ConnectionReset)
|
||||
{
|
||||
LogWrapper.Info("ScaffoldingServer",
|
||||
$"Connection {sessionId} from {clientEndPoint} was closed by the client (Connection Reset).");
|
||||
break;
|
||||
}
|
||||
|
||||
var buffer = readResult.Buffer;
|
||||
var consumedPosition = buffer.Start;
|
||||
|
||||
// REFACTOR: 这是核心修改。我们现在循环处理一个缓冲区,直到无法再解析出完整的帧。
|
||||
while (_TryParseFrame(in buffer, out var requestFrame, out var frameEndPosition))
|
||||
{
|
||||
LogWrapper.Debug("ScaffoldingServer", $"[{sessionId}] Received frame: {requestFrame.TypeInfo}");
|
||||
if (_handlers.TryGetValue(requestFrame.TypeInfo, out var handler))
|
||||
{
|
||||
var (status, responseBody) = await handler
|
||||
.HandleAsync(requestFrame.Body, _context, sessionId, ct).ConfigureAwait(false);
|
||||
|
||||
var responseHeader = new byte[5];
|
||||
responseHeader[0] = status;
|
||||
BinaryPrimitives.WriteUInt32BigEndian(responseHeader.AsSpan(1), (uint)responseBody.Length);
|
||||
await writer.WriteAsync(responseHeader, ct).ConfigureAwait(false);
|
||||
if (responseBody.Length > 0)
|
||||
{
|
||||
await writer.WriteAsync(responseBody, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await writer.FlushAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
LogWrapper.Warn("ScaffoldingServer",
|
||||
$"[{sessionId}] No handler for type: {requestFrame.TypeInfo}");
|
||||
}
|
||||
|
||||
// 将缓冲区切片到已处理帧的末尾,为下一次循环做准备
|
||||
consumedPosition = frameEndPosition;
|
||||
buffer = buffer.Slice(consumedPosition);
|
||||
}
|
||||
|
||||
// 告诉 PipeReader 我们已经检查了直到 readResult.Buffer.End 的所有数据,
|
||||
// 并且我们已经处理了直到 consumedPosition 的数据。
|
||||
reader.AdvanceTo(consumedPosition, buffer.End);
|
||||
|
||||
if (readResult.IsCompleted)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidDataException ex)
|
||||
{
|
||||
LogWrapper.Warn("ScaffoldingServer",
|
||||
$"Malformed packet from {clientEndPoint} on connection {sessionId}. Closing connection. Reason: {ex.Message}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWrapper.Debug("ScaffoldingServer", $"Connection {sessionId} was canceled.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer", $"Unexpected error on connection {sessionId}.");
|
||||
}
|
||||
|
||||
LogWrapper.Debug("ScaffoldingServer", $"Connection {sessionId} from {clientEndPoint} has ended.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _TryParseFrame
|
||||
(in ReadOnlySequence<byte> buffer, out (string TypeInfo, byte[] Body) frame, out SequencePosition consumed)
|
||||
{
|
||||
frame = default;
|
||||
consumed = buffer.Start;
|
||||
|
||||
const int maxTypeLength = 128;
|
||||
const int maxBodyLength = 65536;
|
||||
|
||||
var reader = new SequenceReader<byte>(buffer);
|
||||
|
||||
// 检查头部是否完整 (1字节类型长度 + 4字节内容长度)
|
||||
if (buffer.Length < 1) return false;
|
||||
if (!reader.TryRead(out var typeLength)) return false;
|
||||
|
||||
if (typeLength is 0 or > maxTypeLength)
|
||||
throw new InvalidDataException($"Invalid frame type length: {typeLength}.");
|
||||
|
||||
if (reader.Remaining < typeLength + 4) return false;
|
||||
|
||||
// 读取类型信息
|
||||
Span<byte> typeInfoSpan = stackalloc byte[typeLength];
|
||||
if (!reader.TryCopyTo(typeInfoSpan)) return false;
|
||||
reader.Advance(typeLength);
|
||||
var typeInfo = Encoding.UTF8.GetString(typeInfoSpan);
|
||||
|
||||
// 读取内容长度
|
||||
if (!reader.TryReadBigEndian(out int bodyLength32)) return false;
|
||||
var bodyLength = (uint)bodyLength32;
|
||||
if (bodyLength > maxBodyLength)
|
||||
throw new InvalidDataException($"Frame body length {bodyLength} exceeds maximum of {maxBodyLength}.");
|
||||
|
||||
// 检查内容是否完整
|
||||
if (reader.Remaining < bodyLength) return false;
|
||||
|
||||
// 提取内容
|
||||
var bodyBuffer = reader.Sequence.Slice(reader.Position, bodyLength);
|
||||
var body = bodyBuffer.ToArray();
|
||||
|
||||
// 构造帧
|
||||
frame = (typeInfo, body);
|
||||
|
||||
reader.Advance(bodyLength);
|
||||
consumed = reader.Position;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
LogWrapper.Debug("ScaffoldingServer", "Come into DisposeAsync().");
|
||||
if (!_cts.IsCancellationRequested)
|
||||
{
|
||||
await _cts.CancelAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_listenTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _listenTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer",
|
||||
"An exception occurred while awaiting the listen task during disposal.");
|
||||
}
|
||||
}
|
||||
|
||||
if (_cleanupTask is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cleanupTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "ScaffoldingServer",
|
||||
"An exception occurred while awaiting the cleanup task during disposal.");
|
||||
}
|
||||
}
|
||||
|
||||
_listener.Stop();
|
||||
|
||||
_cts.Dispose();
|
||||
|
||||
LogWrapper.Debug("ScaffoldingServer", "Server and all background tasks stopped gracefully.");
|
||||
|
||||
ServerStopped?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using PCL.Core.Link.Scaffolding.EasyTier;
|
||||
using PCL.Core.Link.Scaffolding.Server.Abstractions;
|
||||
using PCL.Core.App;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server;
|
||||
|
||||
/// <summary>
|
||||
/// Scaffolding Server running context.
|
||||
/// </summary>
|
||||
public class ScaffoldingServerContext : IServerContext
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, TrackedPlayerProfile> _trackedPlayers = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public ConcurrentDictionary<string, TrackedPlayerProfile> TrackedPlayers
|
||||
{
|
||||
get => _trackedPlayers;
|
||||
private init => _trackedPlayers = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PlayerProfile> PlayerProfiles =>
|
||||
_trackedPlayers.Values.Select(player => player.Profile).ToList().AsReadOnly();
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<IReadOnlyList<PlayerProfile>>? PlayerProfilesPing;
|
||||
|
||||
public void OnPlayerProfilesChanged()
|
||||
{
|
||||
var currentProfiles = PlayerProfiles;
|
||||
Task.Run(() => PlayerProfilesPing?.Invoke(currentProfiles));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int MinecraftServerProt { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public LobbyInfo UserLobbyInfo { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string PlayerName { get; }
|
||||
|
||||
private ScaffoldingServerContext(
|
||||
ConcurrentDictionary<string, TrackedPlayerProfile> profiles,
|
||||
int mcPort,
|
||||
LobbyInfo info,
|
||||
string playerName)
|
||||
{
|
||||
TrackedPlayers = profiles;
|
||||
MinecraftServerProt = mcPort;
|
||||
UserLobbyInfo = info;
|
||||
PlayerName = playerName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="ScaffoldingServerContext"/>.
|
||||
/// </summary>
|
||||
/// <param name="playerName">Player name.</param>
|
||||
/// <param name="mcPort">Minecraft shared port.</param>
|
||||
public static ScaffoldingServerContext Create(string playerName, int mcPort)
|
||||
{
|
||||
var machineId = Utils.Secret.Identify.LauncherId;
|
||||
var profile = new PlayerProfile
|
||||
{
|
||||
Name = playerName,
|
||||
MachineId = Utils.Secret.Identify.LauncherId,
|
||||
// Please update ScaffoldingFactory.cs at the same time.
|
||||
Vendor = $"PCL CE {Basics.VersionName}, EasyTier {EasyTierMetadata.CurrentEasyTierVer}",
|
||||
Kind = PlayerKind.HOST
|
||||
};
|
||||
|
||||
var tracked = new TrackedPlayerProfile { Profile = profile, LastSeenUtc = DateTime.UtcNow };
|
||||
|
||||
var roomCode = LobbyCodeGenerator.Generate();
|
||||
|
||||
var profiles = new ConcurrentDictionary<string, TrackedPlayerProfile>();
|
||||
profiles.TryAdd(machineId, tracked);
|
||||
|
||||
return new ScaffoldingServerContext(profiles, mcPort, roomCode, playerName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using PCL.Core.Link.Scaffolding.Client.Models;
|
||||
using System;
|
||||
|
||||
namespace PCL.Core.Link.Scaffolding.Server;
|
||||
|
||||
public record TrackedPlayerProfile
|
||||
{
|
||||
public required PlayerProfile Profile { get; set; }
|
||||
public required DateTime LastSeenUtc { get; set; }
|
||||
};
|
||||
Reference in New Issue
Block a user