初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using PCL.Core.Link.McPing.Model;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.McPing;
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft服务器探测服务接口
|
||||
/// </summary>
|
||||
public interface IMcPingService : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// 异步探测Minecraft服务器信息
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
/// <returns>服务器探测结果,如果探测失败则返回null</returns>
|
||||
Task<McPingResult?> PingAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取服务端点信息
|
||||
/// </summary>
|
||||
IPEndPoint Endpoint { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取主机地址
|
||||
/// </summary>
|
||||
string Host { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取超时时间(毫秒)
|
||||
/// </summary>
|
||||
int Timeout { get; }
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
using PCL.Core.Link.McPing.Model;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.McPing;
|
||||
|
||||
/// <summary>
|
||||
/// 旧版Minecraft协议服务器探测服务实现
|
||||
/// 支持1.6及以下版本的服务器信息查询协议
|
||||
/// </summary>
|
||||
public class LegacyMcPingService : IMcPingService
|
||||
{
|
||||
private readonly IPEndPoint _endpoint;
|
||||
private readonly string _host;
|
||||
private const int DefaultTimeout = 10000;
|
||||
private readonly int _timeout;
|
||||
private bool _disposed;
|
||||
|
||||
public IPEndPoint Endpoint => _endpoint;
|
||||
public string Host => _host;
|
||||
public int Timeout => _timeout;
|
||||
|
||||
public LegacyMcPingService(IPEndPoint endpoint, int timeout = DefaultTimeout)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
_host = _endpoint.Address.ToString();
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
public LegacyMcPingService(string ip, int port = 25565, int timeout = DefaultTimeout)
|
||||
{
|
||||
_endpoint = IPAddress.TryParse(ip, out var ipAddress)
|
||||
? new IPEndPoint(ipAddress, port)
|
||||
: new IPEndPoint(Dns.GetHostAddresses(ip).First(), port);
|
||||
_host = ip;
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行旧版Minecraft协议的服务器探测
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<McPingResult?> PingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: 实现旧版协议的探测逻辑
|
||||
// 这里需要迁移原来McPing类中的PingOldAsync方法逻辑
|
||||
|
||||
using var so = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
using var timeoutCts = new CancellationTokenSource(_timeout);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
linkedCts.Token.Register(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (so.Connected) so.Close();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
/* Ignore */
|
||||
}
|
||||
});
|
||||
|
||||
await so.ConnectAsync(_endpoint, linkedCts.Token);
|
||||
LogWrapper.Debug("LegacyMcPing", $"Connected to {_endpoint}");
|
||||
await using var stream = new NetworkStream(so, false);
|
||||
|
||||
var queryPack = new byte[] { 0xfe, 0x01 };
|
||||
await stream.WriteAsync(queryPack.AsMemory(0, queryPack.Length), linkedCts.Token);
|
||||
var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms, linkedCts.Token);
|
||||
so.Close();
|
||||
var retData = ms.ToArray();
|
||||
if (retData.Length < 21 || (retData.Length >= 21 && retData[0] != 0xff))
|
||||
{
|
||||
LogWrapper.Info("McPing", $"Unknown response from {_endpoint}, ignore");
|
||||
return null;
|
||||
}
|
||||
|
||||
var retRep = Encoding.UTF8.GetString(retData);
|
||||
try
|
||||
{
|
||||
var retPart = retRep.Split(["\0\0\0"], StringSplitOptions.None);
|
||||
retPart = retPart
|
||||
.Select(s => new string([.. s.Where((_, index) => index % 2 == 0)]))
|
||||
.ToArray();
|
||||
if (retPart.Length < 6)
|
||||
return null;
|
||||
return new McPingResult(new McPingVersionResult(retPart[2], int.Parse(retPart[1])),
|
||||
new McPingPlayerResult(int.Parse(retPart[5]), int.Parse(retPart[4]), []), retPart[3], string.Empty, 0,
|
||||
new McPingModInfoResult(string.Empty, []), null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogWrapper.Error(e, "McPing", $"Unable to serialize response from {_endpoint}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using System.Buffers.Binary;
|
||||
using PCL.Core.App.Localization;
|
||||
using PCL.Core.Link.McPing.Model;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Link.McPing;
|
||||
|
||||
/// <summary>
|
||||
/// 现代Minecraft协议服务器探测服务实现
|
||||
/// 支持1.7+版本的服务器信息查询协议
|
||||
/// </summary>
|
||||
public class McPingService : IMcPingService
|
||||
{
|
||||
private readonly IPEndPoint _endpoint;
|
||||
private readonly string _host;
|
||||
private const int DefaultTimeout = 10000;
|
||||
private readonly int _timeout;
|
||||
private bool _disposed;
|
||||
private const string ModuleName = "McPing";
|
||||
|
||||
public IPEndPoint Endpoint => _endpoint;
|
||||
public string Host => _host;
|
||||
public int Timeout => _timeout;
|
||||
|
||||
public McPingService(IPEndPoint endpoint, int timeout = DefaultTimeout)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
_host = _endpoint.Address.ToString();
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
public McPingService(string ip, int port = 25565, int timeout = DefaultTimeout)
|
||||
{
|
||||
_endpoint = IPAddress.TryParse(ip, out var ipAddress)
|
||||
? new IPEndPoint(ipAddress, port)
|
||||
: new IPEndPoint(Dns.GetHostAddresses(ip).First(), port);
|
||||
_host = ip;
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
public McPingService(string host, IPEndPoint endpoint, int timeout = DefaultTimeout)
|
||||
{
|
||||
_endpoint = endpoint;
|
||||
_host = host;
|
||||
_timeout = timeout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行现代Minecraft协议的服务器探测
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task<McPingResult?> PingAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var so = new Socket(SocketType.Stream, ProtocolType.Tcp);
|
||||
using var timeoutCts = new CancellationTokenSource(_timeout);
|
||||
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
|
||||
|
||||
try
|
||||
{
|
||||
LogWrapper.Debug(ModuleName, $"Connecting to {_endpoint}");
|
||||
await so.ConnectAsync(_endpoint.Address, _endpoint.Port, linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWrapper.Error(new TimeoutException(Lang.Text("Tools.ServerQuery.Error.Timeout.Connect")), ModuleName, $"Failed to connect to the {_endpoint}");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogWrapper.Error(e, ModuleName, $"Failed to connect to the {_endpoint}");
|
||||
return null;
|
||||
}
|
||||
|
||||
LogWrapper.Debug(ModuleName, $"Connection established: {_endpoint}");
|
||||
await using var stream = new NetworkStream(so, false);
|
||||
|
||||
var handshakePacket = _BuildHandshakePacket(_host, _endpoint.Port);
|
||||
var statusPacket = _BuildStatusRequestPacket();
|
||||
|
||||
byte[]? statusPayload;
|
||||
long latency = 0;
|
||||
try
|
||||
{
|
||||
await stream.WriteAsync(handshakePacket, linkedCts.Token);
|
||||
LogWrapper.Debug(ModuleName, $"Handshake sent, packet length: {handshakePacket.Length}");
|
||||
|
||||
await stream.WriteAsync(statusPacket, linkedCts.Token);
|
||||
LogWrapper.Debug(ModuleName, $"Status sent, packet length: {statusPacket.Length}");
|
||||
|
||||
var pingTimestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var pingPacket = _BuildPingRequestPacket(pingTimestamp);
|
||||
|
||||
await stream.WriteAsync(pingPacket, linkedCts.Token);
|
||||
LogWrapper.Debug(ModuleName, $"Ping sent, packet length: {pingPacket.Length}");
|
||||
|
||||
(statusPayload, latency) = await _ReadStatusPayloadAsync(stream, linkedCts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
LogWrapper.Error(new TimeoutException(Lang.Text("Tools.ServerQuery.Error.Timeout.ReadWrite")), "McPing", $"Operation timed out on {_endpoint}");
|
||||
return null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogWrapper.Error(e, ModuleName, $"Failed to communicate with {_endpoint}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (so.Connected) so.Shutdown(SocketShutdown.Both);
|
||||
}
|
||||
|
||||
so.Close();
|
||||
|
||||
if (statusPayload is null || statusPayload.Length == 0) throw new InvalidDataException(Lang.Text("Tools.ServerQuery.State.NoInfo"));
|
||||
var retCtx = Encoding.UTF8.GetString(statusPayload);
|
||||
|
||||
var retJson = JsonCompat.ParseNode(retCtx) ?? throw new NullReferenceException(Lang.Text("Tools.ServerQuery.Error.InvalidResponse"));
|
||||
#if DEBUG
|
||||
var resJsonDebug = retJson.DeepClone();
|
||||
if (resJsonDebug is JsonObject jsonObject && jsonObject.ContainsKey("favicon"))
|
||||
{
|
||||
jsonObject["favicon"] = "...";
|
||||
}
|
||||
|
||||
LogWrapper.Debug(ModuleName, resJsonDebug.ToJsonString());
|
||||
#endif
|
||||
// 先处理Description字段,将其转换为字符串形式
|
||||
if (retJson["description"] is JsonObject descObj)
|
||||
{
|
||||
retJson["description"] = _ConvertJNodeToMcString(descObj);
|
||||
}
|
||||
|
||||
var response = JsonSerializer.Deserialize<McPingResult>(retJson, JsonCompat.SerializerOptions);
|
||||
if (response?.Version is null)
|
||||
throw new NullReferenceException(Lang.Text("Tools.ServerQuery.Error.InvalidResponse"));
|
||||
|
||||
response = response with
|
||||
{
|
||||
Latency = latency
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建握手包
|
||||
/// </summary>
|
||||
/// <param name="serverIp">服务器的地址</param>
|
||||
/// <param name="serverPort">服务器的端口</param>
|
||||
/// <returns>返回握手包的字节数组</returns>
|
||||
private byte[] _BuildHandshakePacket(string serverIp, int serverPort)
|
||||
{
|
||||
List<byte> handshake = [];
|
||||
handshake.AddRange(VarIntHelper.Encode(0)); //状态头 表明这是一个握手包
|
||||
handshake.AddRange(VarIntHelper.Encode(772)); //协议头 表明请求客户端的版本
|
||||
var binaryIp = Encoding.UTF8.GetBytes(serverIp);
|
||||
if (binaryIp.Length > 255) throw new Exception(Lang.Text("Tools.ServerQuery.Error.AddressTooLong"));
|
||||
handshake.AddRange(VarIntHelper.Encode((uint)binaryIp.Length)); //服务器地址长度
|
||||
handshake.AddRange(binaryIp); //服务器地址
|
||||
handshake.AddRange(BitConverter.GetBytes((ushort)serverPort).AsEnumerable().Reverse()); //服务器端口
|
||||
handshake.AddRange(VarIntHelper.Encode(1)); //1 表明当前状态为 ping 2 表明当前的状态为连接
|
||||
|
||||
handshake.InsertRange(0, VarIntHelper.Encode((uint)handshake.Count)); //包长度
|
||||
return handshake.ToArray();
|
||||
}
|
||||
|
||||
private byte[] _BuildStatusRequestPacket()
|
||||
{
|
||||
List<byte> statusRequest = [];
|
||||
statusRequest.AddRange(VarIntHelper.Encode(1)); //包长度
|
||||
statusRequest.AddRange(VarIntHelper.Encode(0)); //包 ID
|
||||
return statusRequest.ToArray();
|
||||
}
|
||||
|
||||
private byte[] _BuildPingRequestPacket(long timestamp)
|
||||
{
|
||||
List<byte> pingRequest = [];
|
||||
// Packet ID 使用值为 1 的 VarInt 编码和 8 字节的 long 时间戳
|
||||
pingRequest.AddRange(VarIntHelper.Encode(9));
|
||||
pingRequest.AddRange(VarIntHelper.Encode(1));
|
||||
pingRequest.AddRange(BitConverter.GetBytes(timestamp).AsEnumerable().Reverse());
|
||||
return pingRequest.ToArray();
|
||||
}
|
||||
|
||||
private async Task<(byte[] StatusPayload, long Latency)> _ReadStatusPayloadAsync(Stream stream, CancellationToken cancellationToken)
|
||||
{
|
||||
byte[]? statusPayload = null;
|
||||
long? latency = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (statusPayload is null || latency is null)
|
||||
{
|
||||
var packetLength = checked((int)await VarIntHelper.ReadFromStreamAsync(stream, cancellationToken));
|
||||
LogWrapper.Debug(ModuleName, $"Packet length: {packetLength}");
|
||||
if (packetLength <= 0) throw new InvalidDataException(Lang.Text("Tools.ServerQuery.Error.EmptyPacket"));
|
||||
|
||||
var packetData = await _ReadExactAsync(stream, packetLength, cancellationToken);
|
||||
using var packetStream = new MemoryStream(packetData, writable: false);
|
||||
var packetId = checked((int)await VarIntHelper.ReadFromStreamAsync(packetStream, cancellationToken));
|
||||
LogWrapper.Debug(ModuleName, $"Packet id: {packetId}");
|
||||
|
||||
switch (packetId)
|
||||
{
|
||||
case 0:
|
||||
var jsonLength = checked((int)await VarIntHelper.ReadFromStreamAsync(packetStream, cancellationToken));
|
||||
statusPayload = await _ReadExactAsync(packetStream, jsonLength, cancellationToken);
|
||||
if (packetStream.Position != packetStream.Length)
|
||||
LogWrapper.Warn(ModuleName, $"Status packet contains {packetStream.Length - packetStream.Position} trailing bytes.");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
var pongData = await _ReadExactAsync(packetStream, 8, cancellationToken);
|
||||
if (packetStream.Position != packetStream.Length)
|
||||
LogWrapper.Warn(ModuleName, $"Pong packet contains {packetStream.Length - packetStream.Position} trailing bytes.");
|
||||
latency = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - _ReadInt64BigEndian(pongData);
|
||||
break;
|
||||
|
||||
default:
|
||||
LogWrapper.Warn(ModuleName, $"Ignore unexpected packet type: {packetId}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (EndOfStreamException ex)
|
||||
{
|
||||
if (statusPayload is not null && latency is null)
|
||||
throw new EndOfStreamException(Lang.Text("Tools.ServerQuery.Error.StaleConnection"), ex);
|
||||
|
||||
if (statusPayload is null)
|
||||
throw new EndOfStreamException(Lang.Text("Tools.ServerQuery.Error.IncompleteConnection"), ex);
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
return (statusPayload, latency.Value);
|
||||
}
|
||||
|
||||
private static long _ReadInt64BigEndian(byte[] data)
|
||||
{
|
||||
return data.Length != 8
|
||||
? throw new ArgumentException(Lang.Text("Tools.ServerQuery.Error.PongDataLength"), nameof(data))
|
||||
: BinaryPrimitives.ReadInt64BigEndian(data);
|
||||
}
|
||||
|
||||
private static async Task<byte[]> _ReadExactAsync(Stream stream, int length, CancellationToken cancellationToken)
|
||||
{
|
||||
var buffer = new byte[length];
|
||||
await stream.ReadExactlyAsync(buffer, cancellationToken);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static string _ConvertJNodeToMcString(JsonNode? jsonNode)
|
||||
{
|
||||
if (jsonNode is null) return string.Empty;
|
||||
StringBuilder result = new();
|
||||
Stack<JsonNode> stack = new();
|
||||
stack.Push(jsonNode);
|
||||
|
||||
while (stack.Count > 0)
|
||||
{
|
||||
var current = stack.Pop();
|
||||
|
||||
switch (current.GetValueKind())
|
||||
{
|
||||
// 处理对象
|
||||
case JsonValueKind.Object:
|
||||
{
|
||||
var obj = current.AsObject();
|
||||
// LogWrapper.Debug("McPing",$"Treat {obj} as JObject");
|
||||
// 检查并处理 extra 数组
|
||||
if (obj.TryGetPropertyValue("extra", out var extraNode) && extraNode is JsonArray extraArray)
|
||||
// 逆序压栈保证原始顺序
|
||||
for (var i = extraArray.Count - 1; i >= 0; i--)
|
||||
if (extraArray[i] is not null)
|
||||
stack.Push(extraArray[i]!);
|
||||
// 检查并处理 text 属性
|
||||
if (obj.TryGetPropertyValue("text", out _))
|
||||
{
|
||||
var formatCode = _GetTextStyleString(
|
||||
obj["color"]?.ToString() ?? string.Empty,
|
||||
Convert.ToBoolean(obj["bold"]?.ToString() ?? "false"),
|
||||
Convert.ToBoolean(obj["obfuscated"]?.ToString() ?? "false"),
|
||||
Convert.ToBoolean(obj["strikethrough"]?.ToString() ?? "false"),
|
||||
Convert.ToBoolean(obj["underline"]?.ToString() ?? "false"),
|
||||
Convert.ToBoolean(obj["italic"]?.ToString() ?? "false")
|
||||
);
|
||||
result.Append($"{formatCode}{obj["text"] ?? string.Empty}");
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
// 处理字符串值
|
||||
case JsonValueKind.String:
|
||||
{
|
||||
// LogWrapper.Debug("McPing",$"Treat {value} as JValue");
|
||||
result.Append(current);
|
||||
break;
|
||||
}
|
||||
// 处理数组
|
||||
// 逆序压栈保证原始顺序
|
||||
case JsonValueKind.Array:
|
||||
{
|
||||
var jArr = current.AsArray();
|
||||
// LogWrapper.Debug("McPing",$"Treat {array} as JArray");
|
||||
for (var i = jArr.Count - 1; i >= 0; i--)
|
||||
if (jArr[i] is not null)
|
||||
stack.Push(jArr[i]!);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
LogWrapper.Warn(ModuleName, $"解析到无法处理的 Motd 内容({current.GetValueKind()}):{current}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogWrapper.Debug(ModuleName, $"处理 Motd 内容完成,结果:{result}");
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> _ColorMap = new()
|
||||
{
|
||||
["black"] = "0",
|
||||
["dark_blue"] = "1",
|
||||
["dark_green"] = "2",
|
||||
["dark_aqua"] = "3",
|
||||
["dark_red"] = "4",
|
||||
["dark_purple"] = "5",
|
||||
["gold"] = "6",
|
||||
["gray"] = "7",
|
||||
["dark_gray"] = "8",
|
||||
["blue"] = "9",
|
||||
["green"] = "a",
|
||||
["aqua"] = "b",
|
||||
["red"] = "c",
|
||||
["light_purple"] = "d",
|
||||
["yellow"] = "e",
|
||||
["white"] = "f"
|
||||
};
|
||||
|
||||
private static string _GetTextStyleString(
|
||||
string color,
|
||||
bool bold = false,
|
||||
bool obfuscated = false,
|
||||
bool strikethrough = false,
|
||||
bool underline = false,
|
||||
bool italic = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
if (_ColorMap.TryGetValue(color, out var colorCode)) sb.Append($"§{colorCode}");
|
||||
if (bold) sb.Append("§l");
|
||||
if (italic) sb.Append("§o");
|
||||
// if (obfuscated) sb.Append("§k"); // 暂时别用
|
||||
if (underline) sb.Append("§n");
|
||||
if (strikethrough) sb.Append("§m");
|
||||
if (color.StartsWith('#')) sb.Append(color);
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Net;
|
||||
|
||||
namespace PCL.Core.Link.McPing;
|
||||
|
||||
/// <summary>
|
||||
/// Minecraft服务器探测服务工厂
|
||||
/// 提供统一的服务创建接口
|
||||
/// </summary>
|
||||
public static class McPingServiceFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建现代协议探测服务
|
||||
/// </summary>
|
||||
/// <param name="endpoint">服务器端点</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>IMcPingService实例</returns>
|
||||
public static IMcPingService CreateService(IPEndPoint endpoint, int timeout = 10000)
|
||||
{
|
||||
return new McPingService(endpoint, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建现代协议探测服务
|
||||
/// </summary>
|
||||
/// <param name="ip">服务器IP地址</param>
|
||||
/// <param name="port">服务器端口</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>IMcPingService实例</returns>
|
||||
public static IMcPingService CreateService(string ip, int port = 25565, int timeout = 10000)
|
||||
{
|
||||
return new McPingService(ip, port, timeout);
|
||||
}
|
||||
|
||||
public static IMcPingService CreateService(string host, string? ip, int port = 25565)
|
||||
{
|
||||
return CreateService(host, ip, port, 10000);
|
||||
}
|
||||
|
||||
public static IMcPingService CreateService(string host, string? ip, int port, int timeout)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(ip) && IPAddress.TryParse(ip, out var ipAddress)
|
||||
? new McPingService(host, new IPEndPoint(ipAddress, port), timeout)
|
||||
: new McPingService(host, port, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建旧版协议探测服务
|
||||
/// </summary>
|
||||
/// <param name="endpoint">服务器端点</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>IMcPingService实例</returns>
|
||||
public static IMcPingService CreateLegacyService(IPEndPoint endpoint, int timeout = 10000)
|
||||
{
|
||||
return new LegacyMcPingService(endpoint, timeout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建旧版协议探测服务
|
||||
/// </summary>
|
||||
/// <param name="ip">服务器IP地址</param>
|
||||
/// <param name="port">服务器端口</param>
|
||||
/// <param name="timeout">超时时间(毫秒)</param>
|
||||
/// <returns>IMcPingService实例</returns>
|
||||
public static IMcPingService CreateLegacyService(string ip, int port = 25565, int timeout = 10000)
|
||||
{
|
||||
return new LegacyMcPingService(ip, port, timeout);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingModInfoModResult(
|
||||
[property: JsonPropertyName("modid")] string Id,
|
||||
[property: JsonPropertyName("version")] string Version);
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingModInfoResult(
|
||||
[property: JsonPropertyName("type")] string Type,
|
||||
[property: JsonPropertyName("modList")] List<McPingModInfoModResult> ModList);
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingPlayerResult(
|
||||
[property: JsonPropertyName("max")] int Max,
|
||||
[property: JsonPropertyName("online")] int Online,
|
||||
[property: JsonPropertyName("sample")] List<McPingPlayerSampleResult>? Samples);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingPlayerSampleResult(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("id")] string Id);
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingResult(
|
||||
[property: JsonPropertyName("version")] McPingVersionResult Version,
|
||||
[property: JsonPropertyName("players")] McPingPlayerResult Players,
|
||||
[property: JsonPropertyName("description")] string Description,
|
||||
[property: JsonPropertyName("favicon")] string? Favicon,
|
||||
[property: JsonPropertyName("latency")] long Latency,
|
||||
[property: JsonPropertyName("modinfo")] McPingModInfoResult? ModInfo,
|
||||
[property: JsonPropertyName("preventsChatReports")] bool? PreventsChatReports);
|
||||
@@ -0,0 +1,7 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Link.McPing.Model;
|
||||
|
||||
public record McPingVersionResult(
|
||||
[property: JsonPropertyName("name")] string Name,
|
||||
[property: JsonPropertyName("protocol")] int Protocol);
|
||||
Reference in New Issue
Block a user