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