diff --git a/MinecraftLaunch.Test/Program.cs b/MinecraftLaunch.Test/Program.cs
index e962ee5..c1e26d7 100644
--- a/MinecraftLaunch.Test/Program.cs
+++ b/MinecraftLaunch.Test/Program.cs
@@ -1,30 +1,58 @@
using MinecraftLaunch;
using MinecraftLaunch.Components.Resolver;
using MinecraftLaunch.Components.Installer;
+using MinecraftLaunch.Components.Watcher;
+using MinecraftLaunch.Classes.Models.Event;
-GameResolver gameResolver = new("C:\\Users\\w\\Downloads\\.minecraft");
+# region ServerPing
-VanlliaInstaller vanlliaInstaller = new(gameResolver, "1.12.2");
-vanlliaInstaller.ProgressChanged += (_, args) => {
- Console.WriteLine($"{args.Progress * 100:0.00} - {args.Status} - {args.ProgressStatus}");
-};
+ServerPingWatcher serverPingWatcher = new(25565, "mc.163mc.cn", 47);
-await vanlliaInstaller.InstallAsync();
+serverPingWatcher.ServerConnectionProgressChanged += OnServerConnectionProgressChanged;
-Console.WriteLine();
-Console.WriteLine();
-Console.WriteLine();
-Console.WriteLine();
-Console.WriteLine();
+serverPingWatcher.ServerLatencyChanged += (_, args) => {
+ Console.WriteLine($"{args.Latency}ms");
+};
-ForgeInstaller forgeInstaller = new(gameResolver.GetGameEntity("1.12.2"),
- (await ForgeInstaller.EnumerableFromVersionAsync("1.12.2")).First(),
- "C:\\Program Files\\Java\\jdk1.8.0_301\\bin\\javaw.exe",
- "1.12.2-forge-114514");
+await serverPingWatcher.StartAsync();
-forgeInstaller.ProgressChanged += (_, args) => {
+void OnServerConnectionProgressChanged(object? sender, ProgressChangedEventArgs args) {
Console.WriteLine($"{args.Progress * 100:0.00} - {args.Status} - {args.ProgressStatus}");
-};
+ if (args.Status == TaskStatus.Canceled) {
+ serverPingWatcher.ServerConnectionProgressChanged -= OnServerConnectionProgressChanged;
+ }
+}
+
+#endregion
+
+# region Forge Install
+
+//GameResolver gameResolver = new("C:\\Users\\w\\Downloads\\.minecraft");
+
+//VanlliaInstaller vanlliaInstaller = new(gameResolver, "1.12.2");
+//vanlliaInstaller.ProgressChanged += (_, args) => {
+// Console.WriteLine($"{args.Progress * 100:0.00} - {args.Status} - {args.ProgressStatus}");
+//};
+
+//await vanlliaInstaller.InstallAsync();
+
+//Console.WriteLine();
+//Console.WriteLine();
+//Console.WriteLine();
+//Console.WriteLine();
+//Console.WriteLine();
+
+//ForgeInstaller forgeInstaller = new(gameResolver.GetGameEntity("1.12.2"),
+// (await ForgeInstaller.EnumerableFromVersionAsync("1.12.2")).First(),
+// "C:\\Program Files\\Java\\jdk1.8.0_301\\bin\\javaw.exe",
+// "1.12.2-forge-114514");
+
+//forgeInstaller.ProgressChanged += (_, args) => {
+// Console.WriteLine($"{args.Progress * 100:0.00} - {args.Status} - {args.ProgressStatus}");
+//};
+
+//await forgeInstaller.InstallAsync();
+
+#endregion
-await forgeInstaller.InstallAsync();
Console.ReadKey();
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Interfaces/IWatcher.cs b/MinecraftLaunch/Classes/Interfaces/IWatcher.cs
new file mode 100644
index 0000000..177252a
--- /dev/null
+++ b/MinecraftLaunch/Classes/Interfaces/IWatcher.cs
@@ -0,0 +1,8 @@
+namespace MinecraftLaunch.Classes.Interfaces;
+
+///
+/// 监视器统一接口
+///
+public interface IWatcher {
+ void Start();
+}
diff --git a/MinecraftLaunch/Classes/Models/Event/ServerLatencyChangedEventArgs.cs b/MinecraftLaunch/Classes/Models/Event/ServerLatencyChangedEventArgs.cs
new file mode 100644
index 0000000..79850db
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/Event/ServerLatencyChangedEventArgs.cs
@@ -0,0 +1,7 @@
+using MinecraftLaunch.Classes.Models.ServerPing;
+
+namespace MinecraftLaunch.Classes.Models.Event;
+public sealed class ServerLatencyChangedEventArgs : EventArgs {
+ public long Latency { get; set; }
+ public PingPayload Response { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Models/ServerPing/PingPayload.cs b/MinecraftLaunch/Classes/Models/ServerPing/PingPayload.cs
new file mode 100644
index 0000000..867a188
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/ServerPing/PingPayload.cs
@@ -0,0 +1,15 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MinecraftLaunch.Classes.Models.ServerPing;
+
+public sealed class PingPayload {
+ [JsonPropertyName("favicon")] public string Icon { get; set; }
+ [JsonPropertyName("version")] public VersionPayload Version { get; set; }
+ [JsonPropertyName("players")] public PlayersPayload Players { get; set; }
+ [JsonPropertyName("modinfo")] public ServerPingModInfo ModInfo { get; set; }
+ [JsonPropertyName("description")] public JsonElement Description { get; set; }
+}
+
+[JsonSerializable(typeof(PingPayload))]
+sealed partial class PingPayloadContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Models/ServerPing/Player.cs b/MinecraftLaunch/Classes/Models/ServerPing/Player.cs
new file mode 100644
index 0000000..4c5929e
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/ServerPing/Player.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+
+namespace MinecraftLaunch.Classes.Models.ServerPing;
+
+public sealed class Player {
+ [JsonPropertyName("id")] public string Id { get; set; }
+ [JsonPropertyName("name")] public string Name { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Models/ServerPing/PlayersPayload.cs b/MinecraftLaunch/Classes/Models/ServerPing/PlayersPayload.cs
new file mode 100644
index 0000000..4609f2f
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/ServerPing/PlayersPayload.cs
@@ -0,0 +1,9 @@
+using System.Text.Json.Serialization;
+
+namespace MinecraftLaunch.Classes.Models.ServerPing;
+
+public sealed class PlayersPayload {
+ [JsonPropertyName("max")] public int Max { get; set; }
+ [JsonPropertyName("online")] public int Online { get; set; }
+ [JsonPropertyName("sample")] public Player[] Sample { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Models/ServerPing/ServerPingModInfo.cs b/MinecraftLaunch/Classes/Models/ServerPing/ServerPingModInfo.cs
new file mode 100644
index 0000000..ab8727e
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/ServerPing/ServerPingModInfo.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+
+namespace MinecraftLaunch.Classes.Models.ServerPing;
+
+public sealed class ServerPingModInfo {
+ [JsonPropertyName("type")] public string Type { get; set; }
+ [JsonPropertyName("modList")] public IEnumerable ModList { get; set; }
+}
+
+public sealed class ModInfo {
+ [JsonPropertyName("modid")] public string ModId { get; set; }
+ [JsonPropertyName("version")] public string Version { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Classes/Models/ServerPing/VersionPayload.cs b/MinecraftLaunch/Classes/Models/ServerPing/VersionPayload.cs
new file mode 100644
index 0000000..108c6c7
--- /dev/null
+++ b/MinecraftLaunch/Classes/Models/ServerPing/VersionPayload.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+
+namespace MinecraftLaunch.Classes.Models.ServerPing;
+
+public sealed class VersionPayload {
+ [JsonPropertyName("name")] public string Name { get; set; }
+ [JsonPropertyName("protocol")] public int Protocol { get; set; }
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/Components/Watcher/GameProcessWatcher.cs b/MinecraftLaunch/Components/Watcher/GameProcessWatcher.cs
index 03399cc..cd55fb7 100644
--- a/MinecraftLaunch/Components/Watcher/GameProcessWatcher.cs
+++ b/MinecraftLaunch/Components/Watcher/GameProcessWatcher.cs
@@ -1,4 +1,5 @@
-using System.Diagnostics;
+using System;
+using System.Diagnostics;
using MinecraftLaunch.Classes.Interfaces;
using MinecraftLaunch.Classes.Models.Event;
@@ -7,7 +8,7 @@ namespace MinecraftLaunch.Components.Watcher;
///
/// 游戏进程监视器
///
-public class GameProcessWatcher : IGameProcessWatcher {
+public class GameProcessWatcher : IWatcher, IGameProcessWatcher {
public Process Process { get; }
public IEnumerable Arguments { get; }
@@ -19,13 +20,17 @@ public class GameProcessWatcher : IGameProcessWatcher {
public GameProcessWatcher(Process process, IEnumerable arguments) {
Process = process;
Arguments = arguments;
- process.Exited += OnExited;
- process.ErrorDataReceived += OnOutputDataReceived;
- process.OutputDataReceived += OnOutputDataReceived;
- process.Start();
+ Start();
+ }
+
+ public void Start() {
+ Process.Exited += OnExited;
+ Process.ErrorDataReceived += OnOutputDataReceived;
+ Process.OutputDataReceived += OnOutputDataReceived;
- process.BeginErrorReadLine();
- process.BeginOutputReadLine();
+ Process.Start();
+ Process.BeginErrorReadLine();
+ Process.BeginOutputReadLine();
}
private void OnExited(object sender, EventArgs e) {
diff --git a/MinecraftLaunch/Components/Watcher/ServerPingWatcher.cs b/MinecraftLaunch/Components/Watcher/ServerPingWatcher.cs
new file mode 100644
index 0000000..b79da0d
--- /dev/null
+++ b/MinecraftLaunch/Components/Watcher/ServerPingWatcher.cs
@@ -0,0 +1,220 @@
+using System.Text;
+using System.Net.Sockets;
+using MinecraftLaunch.Classes.Interfaces;
+using System.Diagnostics;
+using System.Text.Json;
+using MinecraftLaunch.Classes.Models.ServerPing;
+using MinecraftLaunch.Classes.Models.Event;
+
+namespace MinecraftLaunch.Components.Watcher;
+
+///
+/// 服务器 Ping 监视器
+///
+public sealed class ServerPingWatcher(ushort port, string address, int versionId, CancellationTokenSource tokenSource = default) : IWatcher {
+ private int _offset;
+ private List _buffer;
+ private NetworkStream _stream;
+ private CancellationTokenSource _cancellationTokenSource = tokenSource;
+
+ public event EventHandler ServerLatencyChanged;
+ public event EventHandler ServerConnectionProgressChanged;
+
+ public ushort Port => port;
+ public string Address => address;
+ public int VersionId => versionId;
+
+ public void Cancel() {
+ _cancellationTokenSource?.Cancel();
+ }
+
+ public void Start() => _ = StartAsync();
+
+ public override string ToString() => $"{Address}:{Port}";
+
+ public async ValueTask StartAsync() {
+ if (_cancellationTokenSource is null) {
+ _cancellationTokenSource = new();
+ }
+
+ while (!_cancellationTokenSource.IsCancellationRequested) {
+ await PingAsync();
+ await Task.Delay(TimeSpan.FromSeconds(3));
+ }
+ }
+
+ public async ValueTask PingAsync() {
+ using var client = new TcpClient {
+ SendTimeout = 5000,
+ ReceiveTimeout = 5000
+ };
+
+ var sw = new Stopwatch();
+ var timeOut = TimeSpan.FromSeconds(3);
+ using var cts = new CancellationTokenSource(timeOut);
+
+ sw.Start();
+ cts.CancelAfter(timeOut);
+
+ try {
+ await client.ConnectAsync(Address, Port, cts.Token);
+ } catch (TaskCanceledException) {
+ throw new OperationCanceledException($"Server {this} connection failed, connection timed out ({timeOut.Seconds}s)。", cts.Token);
+ }
+
+ sw.Stop();
+
+ ReportProgress(0.1d, "Connecting to server", TaskStatus.Created);
+
+ if (!client.Connected) {
+ ReportProgress(0.1d, "Unable to connect to server", TaskStatus.Faulted);
+ return;
+ }
+
+ _buffer = new List();
+ _stream = client.GetStream();
+
+ ReportProgress(0.3d, "Sending request", TaskStatus.Running);
+
+ /*
+ * Send a "Handshake" packet
+ * http://wiki.vg/Server_List_Ping#Ping_Process
+ */
+ WriteVarInt(VersionId == 0 ? 47 : VersionId);
+ WriteString(Address);
+ WriteShort(Port);
+ WriteVarInt(1);
+ await Flush(0);
+
+ /*
+ * Send a "Status Request" packet
+ * http://wiki.vg/Server_List_Ping#Ping_Process
+ */
+ await Flush(0);
+
+ /*
+ * If you are using a modded server then use a larger buffer to account,
+ * see link for explanation and a motd to HTML snippet
+ * https://gist.github.com/csh/2480d14fbbb33b4bbae3#gistcomment-2672658
+ */
+ var batch = new byte[1024];
+ await using var ms = new MemoryStream();
+ var remaining = 0;
+ var flag = false;
+
+ var latency = sw.ElapsedMilliseconds;
+
+ do {
+ _offset = 0;
+ var readLength = await _stream.ReadAsync(batch.AsMemory());
+ await ms.WriteAsync(batch.AsMemory(0, readLength), cts.Token);
+ if (!flag) {
+ var packetLength = ReadVarInt(ms.ToArray());
+ remaining = packetLength - _offset;
+ flag = true;
+ }
+
+ if (readLength == 0 && remaining != 0)
+ continue;
+
+ remaining -= readLength;
+ } while (remaining > 0);
+
+ var buffer = ms.ToArray();
+ var length = ReadVarInt(buffer);
+ var packet = ReadVarInt(buffer);
+ var jsonLength = ReadVarInt(buffer);
+
+ var json = ReadString(buffer, jsonLength);
+ var ping = JsonSerializer.Deserialize(json, PingPayloadContext.Default.PingPayload);
+
+ ReportProgress(1.0d, $"Server {this} successfully connected", TaskStatus.Canceled);
+ ReportLatency(latency, ping);
+ }
+
+ #region Read/Write methods
+
+ private byte ReadByte(IReadOnlyList buffer) {
+ var b = buffer[_offset];
+ _offset += 1;
+ return b;
+ }
+
+ private byte[] Read(byte[] buffer, int length) {
+ var data = new byte[length];
+ Array.Copy(buffer, _offset, data, 0, length);
+ _offset += length;
+ return data;
+ }
+
+ private int ReadVarInt(IReadOnlyList buffer) {
+ var value = 0;
+ var size = 0;
+ int b;
+ while (((b = ReadByte(buffer)) & 0x80) == 0x80) {
+ value |= (b & 0x7F) << (size++ * 7);
+ if (size > 5) throw new IOException("This VarInt is an imposter!");
+ }
+
+ return value | ((b & 0x7F) << (size * 7));
+ }
+
+ private string ReadString(byte[] buffer, int length) {
+ var data = Read(buffer, length);
+ return Encoding.UTF8.GetString(data);
+ }
+
+ private void WriteVarInt(int value) {
+ while ((value & 128) != 0) {
+ _buffer.Add((byte)((value & 127) | 128));
+ value = (int)(uint)value >> 7;
+ }
+
+ _buffer.Add((byte)value);
+ }
+
+ private void WriteShort(ushort value) {
+ _buffer.AddRange(BitConverter.GetBytes(value));
+ }
+
+ private void WriteString(string data) {
+ var buffer = Encoding.UTF8.GetBytes(data);
+ WriteVarInt(buffer.Length);
+ _buffer.AddRange(buffer);
+ }
+
+ private async Task Flush(int id = -1) {
+ var buffer = _buffer.ToArray();
+ _buffer.Clear();
+
+ var add = 0;
+ var packetData = new[] { (byte)0x00 };
+ if (id >= 0) {
+ WriteVarInt(id);
+ packetData = _buffer.ToArray();
+ add = packetData.Length;
+ _buffer.Clear();
+ }
+
+ WriteVarInt(buffer.Length + add);
+ var bufferLength = _buffer.ToArray();
+ _buffer.Clear();
+
+ await _stream.WriteAsync(bufferLength.AsMemory());
+ await _stream.WriteAsync(packetData.AsMemory());
+ await _stream.WriteAsync(buffer.AsMemory());
+ }
+
+ private void ReportLatency(long latency, PingPayload pingPayload) {
+ ServerLatencyChanged?.Invoke(this, new() {
+ Latency = latency,
+ Response = pingPayload
+ });
+ }
+
+ private void ReportProgress(double progress, string progressStatus, TaskStatus status) {
+ ServerConnectionProgressChanged?.Invoke(this, new(status, progress, progressStatus));
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/MinecraftLaunch/MinecraftLaunch.csproj b/MinecraftLaunch/MinecraftLaunch.csproj
index 2182e49..60d8e8b 100644
--- a/MinecraftLaunch/MinecraftLaunch.csproj
+++ b/MinecraftLaunch/MinecraftLaunch.csproj
@@ -1,8 +1,8 @@
- 3.0.05
+ 3.0.06
-
+
https://github.com/Blessing-Studio/MinecraftLaunch
https://github.com/Blessing-Studio/MinecraftLaunch
@@ -16,7 +16,7 @@
MinecraftLaunch
disable
-
+
True
@@ -28,4 +28,4 @@
-
+
\ No newline at end of file