diff --git a/.gitignore b/.gitignore index 9491a2f..ffae5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,11 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +# NectarRcon Config +config.json +servers.json +passwords.json + # User-specific files *.rsuser *.suo diff --git a/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs b/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs index a1ac925..0bca3cc 100644 --- a/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs +++ b/NectarRCON.Adapter.Minecraft/MinecraftRconClient.cs @@ -1,6 +1,7 @@ using NectarRCON.Export.Client; using NectarRCON.Export.Interfaces; using System.ComponentModel; +using System.Text; namespace NectarRCON.Adapter.Minecraft { @@ -11,8 +12,13 @@ public class MinecraftRconClient : BaseTcpClient, IRconAdapter private static readonly int MaxMessageSize = 4110; private readonly MemoryStream _buffer = new(); - private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1); - private int lastId = 0; + private readonly SemaphoreSlim _semaphore = new(1); + private int _lastId; + /// + /// 编码 + /// 默认编码 UTF8 + /// + private new Encoding _encoding = Encoding.UTF8; public void Disconnect() { @@ -56,6 +62,14 @@ public string Run(string command) } } + public Encoding GetEncoding() + => _encoding; + + public void SetEncoding(Encoding encoding) + { + _encoding = encoding; + } + public bool Connect(string address, int port) { _semaphore.Wait(); @@ -76,7 +90,7 @@ public bool Authenticate(string password) try { Packet packet = Send(new Packet(PacketType.Authenticate, password)); - return packet.Id == lastId; + return packet.Id == _lastId; } finally { @@ -86,9 +100,9 @@ public bool Authenticate(string password) private Packet Send(Packet packet) { - Interlocked.Increment(ref lastId); - packet.SetId(lastId); - return PacketEncoder.Decode(Send(packet.Encode())); + Interlocked.Increment(ref _lastId); + packet.SetId(_lastId); + return PacketEncoder.Decode(Send(packet.Encode(_encoding)), _encoding); } } } \ No newline at end of file diff --git a/NectarRCON.Adapter.Minecraft/Packet.cs b/NectarRCON.Adapter.Minecraft/Packet.cs index 29fc944..e118be8 100644 --- a/NectarRCON.Adapter.Minecraft/Packet.cs +++ b/NectarRCON.Adapter.Minecraft/Packet.cs @@ -27,10 +27,10 @@ public void SetId(int id) Id = id; } - public byte[] Encode() + public byte[] Encode(Encoding? encoding = null) { List bytes = new List(); - var data = Encoding.UTF8.GetBytes(Body); + var data = (encoding?? Encoding.UTF8).GetBytes(Body); bytes.AddRange(BitConverter.GetBytes(PacketEncoder.HeaderLength + data.Length)); bytes.AddRange(BitConverter.GetBytes(Id)); bytes.AddRange(BitConverter.GetBytes((int)Type)); diff --git a/NectarRCON.Adapter.Minecraft/PacketEncoder.cs b/NectarRCON.Adapter.Minecraft/PacketEncoder.cs index b105953..be3b5c6 100644 --- a/NectarRCON.Adapter.Minecraft/PacketEncoder.cs +++ b/NectarRCON.Adapter.Minecraft/PacketEncoder.cs @@ -10,24 +10,24 @@ public enum PacketType : int Authenticate // 3: Login } - public class PacketEncoder + public abstract class PacketEncoder { public const int HeaderLength = 10; - public static byte[] Encode(Packet msg) + public static byte[] Encode(Packet msg, Encoding? encoding = null) { List bytes = new List(); bytes.AddRange(BitConverter.GetBytes(msg.Length)); bytes.AddRange(BitConverter.GetBytes(msg.Id)); bytes.AddRange(BitConverter.GetBytes((int)msg.Type)); - bytes.AddRange(Encoding.UTF8.GetBytes(msg.Body)); + bytes.AddRange((encoding??Encoding.UTF8).GetBytes(msg.Body)); bytes.AddRange(new byte[] { 0, 0 }); return bytes.ToArray(); } - public static Packet Decode(byte[] bytes) + public static Packet Decode(byte[] bytes, Encoding? encoding = null) { if (bytes.Length < HeaderLength) { throw new ArgumentException("packet length too short"); } int len = BitConverter.ToInt32(bytes, 0); @@ -37,7 +37,7 @@ public static Packet Decode(byte[] bytes) string body = string.Empty; if (bodyLen > 0) { - body = Encoding.UTF8.GetString(bytes, 12, bodyLen); + body = (encoding??Encoding.UTF8).GetString(bytes, 12, bodyLen); } return new Packet(len, id, (PacketType)type, body); } diff --git a/NectarRCON.Export/Interfaces/IRconAdapter.cs b/NectarRCON.Export/Interfaces/IRconAdapter.cs index 4cc0106..edfd9d6 100644 --- a/NectarRCON.Export/Interfaces/IRconAdapter.cs +++ b/NectarRCON.Export/Interfaces/IRconAdapter.cs @@ -1,4 +1,6 @@ -namespace NectarRCON.Export.Interfaces; +using System.Text; + +namespace NectarRCON.Export.Interfaces; /// /// Rcon协议兼容接口 /// @@ -30,4 +32,7 @@ public interface IRconAdapter : IDisposable /// string Run(string command); + + Encoding GetEncoding(); + void SetEncoding(Encoding encoding); } diff --git a/NectarRCON.Export/NectarRCON.Export.csproj b/NectarRCON.Export/NectarRCON.Export.csproj index cfadb03..88eb041 100644 --- a/NectarRCON.Export/NectarRCON.Export.csproj +++ b/NectarRCON.Export/NectarRCON.Export.csproj @@ -1,9 +1,13 @@ - net7.0 + net7.0-windows enable enable + + + + diff --git a/NectarRCON.Tests/NectarRCON.Tests.csproj b/NectarRCON.Tests/NectarRCON.Tests.csproj index b53a7d2..49e183a 100644 --- a/NectarRCON.Tests/NectarRCON.Tests.csproj +++ b/NectarRCON.Tests/NectarRCON.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/NectarRCON.Tests/UpdaterTests.cs b/NectarRCON.Tests/UpdaterTests.cs new file mode 100644 index 0000000..396921e --- /dev/null +++ b/NectarRCON.Tests/UpdaterTests.cs @@ -0,0 +1,40 @@ +using NectarRCON.Updater; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NectarRCON.Tests +{ + [TestClass] + public class UpdaterTests + { + [TestMethod] + public void Github() + { + IUpdater updater = new GithubUpdater(); + updater.SetVersion("NectarRcon-x86-1.0.0"); + updater.IsLatestVersion(); + } + + [TestMethod] + public void AppVersionTest() + { + AppVersion versionA = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta1"); + AppVersion versionB = AppVersion.ParseVersion("TestApp-x64-1.0.0-beta2"); + + Assert.IsTrue(versionA.Equals(versionA)); + Assert.IsFalse(versionA.Equals(versionB)); + +#pragma warning disable CS1718 // 对同一变量进行了比较 + Assert.IsTrue(versionA == versionA); + Assert.IsFalse(versionA != versionA); + Assert.IsFalse(versionA > versionA); +#pragma warning restore CS1718 // 对同一变量进行了比较 + + Assert.IsTrue(versionB > versionA); + Assert.IsFalse(versionB < versionA); + } + } +} diff --git a/NectarRCON.Updater/AppVersion.cs b/NectarRCON.Updater/AppVersion.cs new file mode 100644 index 0000000..618fe16 --- /dev/null +++ b/NectarRCON.Updater/AppVersion.cs @@ -0,0 +1,105 @@ +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; + +namespace NectarRCON.Updater +{ + public class AppVersion + { + public string AppName { get; set; } = string.Empty; + public int Version { get; set; } + public int Major { get;set; } + public int Minor { get;set; } + public int Patch { get;set; } + public int? Build { get; set; } + public string PreReleaseType { get; set; } = string.Empty; + public string Platform { get; set; } = string.Empty; + public bool IsPreRelease + => !string.IsNullOrEmpty(PreReleaseType); + + public override string ToString() + { + return $"{AppName}-{Platform}-{Major}.{Minor}.{Patch}" + (IsPreRelease ? $"-{PreReleaseType}{Build}" : string.Empty); + } + + public override bool Equals(object? obj) + { + return obj?.ToString() == ToString(); + } + + public static bool operator <(AppVersion a, AppVersion b) + { + return a.Version < b.Version || (a.Build ?? 0) < (b.Build ?? 0); + } + + public static bool operator >(AppVersion a, AppVersion b) + { + return a.Version > b.Version || (a.Build ?? 0) > (b.Build ?? 0); + } + + public static bool operator ==(AppVersion a, AppVersion b) + { + return a.Version == b.Version && (a.Build ?? 0) == (b.Build ?? 0); + } + + public static bool operator !=(AppVersion a, AppVersion b) + { + return a.Version != b.Version || (a.Build ?? 0) != (b.Build ?? 0); + } + + private AppVersion() { } + + public static AppVersion ParseVersion(string version) + { + string[] versionParts = version.Split("-"); + if (versionParts.Length > 2) + { + AppVersion result = new(); + string name = versionParts[0]; + string platform = versionParts[1]; + string ver = versionParts[2]; + string preRelease = string.Empty; + + if (versionParts.Length > 3) + { + preRelease = versionParts[3]; + } + + Regex versionRegex = new(@"(?\d+)\.(?\d+)\.(?\d+)"); + Match versionMatch = versionRegex.Match(ver); + + if (versionMatch.Success) + { + result.Version = int.Parse(versionMatch.Groups["major"].Value + versionMatch.Groups["minor"].Value + versionMatch.Groups["patch"].Value); + result.Major = int.Parse(versionMatch.Groups["major"].Value); + result.Minor = int.Parse(versionMatch.Groups["minor"].Value); + result.Patch = int.Parse(versionMatch.Groups["patch"].Value); + } + + Regex preReleaseRegex = new(@"(?[a-zA-Z]+)(?\d+)"); + Match preReleaseMatch = preReleaseRegex.Match(preRelease); + + if (preReleaseMatch.Success) + { + if (preReleaseMatch.Groups["build"].Success) + { + result.Build = int.Parse(preReleaseMatch.Groups["build"].Value); + } + if (preReleaseMatch.Groups["preRelease"].Success) + { + result.PreReleaseType = preReleaseMatch.Groups["preRelease"].Value; + } + } + + result.Platform = platform; + result.AppName = name; + return result; + } + throw new InvalidOperationException("Invalid version format"); + } + + public override int GetHashCode() + { + return RuntimeHelpers.GetHashCode(ToString()); + } + } +} diff --git a/NectarRCON.Updater/GithubUpdater.cs b/NectarRCON.Updater/GithubUpdater.cs new file mode 100644 index 0000000..1f43cf3 --- /dev/null +++ b/NectarRCON.Updater/GithubUpdater.cs @@ -0,0 +1,91 @@ +using NectarRCON.Updater.Model; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; + +namespace NectarRCON.Updater +{ + public class GithubUpdater : IUpdater + { + private static readonly HttpClient _client = new() + { + BaseAddress = new Uri("https://api.github.com/repos/zkhssb/NectarRcon/") + }; + private bool _preEnable = false; + private AppVersion? _version; + + /// + /// 获取最新版本, null为没找到 + /// + /// 是否允许pre版本 + private AppVersion? GetLatestVersion(bool enablePre) + { + if (_version is null) + return null; + using(HttpRequestMessage request = new(HttpMethod.Get, "releases/latest")) + { + request.Headers.Add("User-Agent", $"{_version.AppName}-AppUpdater"); + using(HttpResponseMessage response = _client.Send(request)) + { + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(response.StatusCode.ToString()); + string resultString = string.Empty; + Task.Run(async () => + { + resultString = await response.Content.ReadAsStringAsync(); + }).Wait(); + Release release = JsonSerializer.Deserialize(resultString) ?? throw new JsonException(); + foreach(var asset in release.Assets) + { + string fileName = Path.GetFileNameWithoutExtension(asset.Name); + try + { + fileName = "NectarRcon-x86-1.0.0-beta2"; + AppVersion version = AppVersion.ParseVersion(fileName); + if(version.AppName.ToLower() == _version.AppName.ToLower() && version.Platform.ToLower() == _version.Platform.ToLower()) + { + if (version.IsPreRelease && !enablePre) + continue; + if (version > _version) + { + return version; + } + } + } + catch (InvalidOperationException) { } // Invalid version format + } + return null; + } + } + } + + public bool IsLatestVersion() + { + GetLatestVersion(_preEnable); + return true; + } + + public void Setup() + { + throw new NotImplementedException(); + } + + public void SetVersion(string version) + { + _version = AppVersion.ParseVersion(version); + } + + public void SetPreEnable(bool value) + { + _preEnable = value; + } + + public AppVersion GetLatestVersion() + { + throw new NotImplementedException(); + } + } +} diff --git a/NectarRCON.Updater/IUpdater.cs b/NectarRCON.Updater/IUpdater.cs new file mode 100644 index 0000000..ca0067f --- /dev/null +++ b/NectarRCON.Updater/IUpdater.cs @@ -0,0 +1,26 @@ +namespace NectarRCON.Updater +{ + public interface IUpdater + { + /// + /// 设置版本 + /// + void SetVersion(string version); + /// + /// 是最新版 + /// + bool IsLatestVersion(); + /// + /// 获取最新版本 + /// + AppVersion GetLatestVersion(); + /// + /// 开始安装 + /// + void Setup(); + /// + /// 设置是否启用获取预发布版本更新 + /// + void SetPreEnable(bool value); + } +} diff --git a/NectarRCON.Updater/Model/Asset.cs b/NectarRCON.Updater/Model/Asset.cs new file mode 100644 index 0000000..235c83b --- /dev/null +++ b/NectarRCON.Updater/Model/Asset.cs @@ -0,0 +1,14 @@ +using System.Text.Json.Serialization; + +namespace NectarRCON.Updater.Model +{ + public class Asset + { + [JsonPropertyName("name")] + public required string Name { get; set; } + [JsonPropertyName("url")] + public required string Url { get; set; } + [JsonPropertyName("created_at")] + public required DateTime CreatedAt { get; set; } + } +} diff --git a/NectarRCON.Updater/Model/Release.cs b/NectarRCON.Updater/Model/Release.cs new file mode 100644 index 0000000..c753d84 --- /dev/null +++ b/NectarRCON.Updater/Model/Release.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace NectarRCON.Updater.Model +{ + public class Release + { + [JsonPropertyName("tag_name")] + public required string TagName { get; set; } + + [JsonPropertyName("name")] + public required string Name { get; set; } + + [JsonPropertyName("created_at")] + public required DateTime CreatedAt { get; set; } + + [JsonPropertyName("assets")] + public required IEnumerable Assets { get; set; } + + [JsonPropertyName("body")] + public required string Body { get; set; } + } +} diff --git a/NectarRCON.Updater/NectarRCON.Updater.csproj b/NectarRCON.Updater/NectarRCON.Updater.csproj new file mode 100644 index 0000000..cb6a55a --- /dev/null +++ b/NectarRCON.Updater/NectarRCON.Updater.csproj @@ -0,0 +1,9 @@ + + + + net7.0-windows + enable + enable + + + diff --git a/NectarRCON.sln b/NectarRCON.sln index 2a952cb..d3a99e5 100644 --- a/NectarRCON.sln +++ b/NectarRCON.sln @@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Export", "Nectar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Adapter.Minecraft", "NectarRCON.Adapter.Minecraft\NectarRCON.Adapter.Minecraft.csproj", "{D4B97850-FF59-4AA1-A19F-2C22F80A8B20}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NectarRCON.Core", "NectarRCON.Core\NectarRCON.Core.csproj", "{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NectarRCON.Core", "NectarRCON.Core\NectarRCON.Core.csproj", "{8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NectarRCON.Updater", "NectarRCON.Updater\NectarRCON.Updater.csproj", "{D6C910A7-3590-492B-9CA1-C3D586FF2C41}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +41,10 @@ Global {8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {8C15668B-69F3-4138-BCE6-0BB6A65F3B2F}.Release|Any CPU.Build.0 = Release|Any CPU + {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6C910A7-3590-492B-9CA1-C3D586FF2C41}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/NectarRCON/App.xaml b/NectarRCON/App.xaml index c5ea934..e71476f 100644 --- a/NectarRCON/App.xaml +++ b/NectarRCON/App.xaml @@ -14,7 +14,7 @@ - 1.0.0-beta3 + 1.0.0-beta4 diff --git a/NectarRCON/App.xaml.cs b/NectarRCON/App.xaml.cs index 506de6a..314c866 100644 --- a/NectarRCON/App.xaml.cs +++ b/NectarRCON/App.xaml.cs @@ -7,7 +7,9 @@ using NectarRCON.Windows; using System; using System.Linq; +using System.Text; using System.Windows; +using NectarRCON.Dp; using Wpf.Ui.Mvvm.Contracts; using Wpf.Ui.Mvvm.Services; @@ -66,6 +68,13 @@ public static T GetService(Type type) private async void OnStartup(object sender, StartupEventArgs e) { + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + foreach (var rconEncoding in Enum.GetValues()) + { + rconEncoding.GetEncoding(); + } + await _host.StartAsync(); } diff --git a/NectarRCON/Dp/DpFile.cs b/NectarRCON/Dp/DpFile.cs new file mode 100644 index 0000000..349bdf0 --- /dev/null +++ b/NectarRCON/Dp/DpFile.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace NectarRCON.Dp; + +/// +/// 数据持久化文件 +/// +public abstract class DpFile +{ + /// + /// 文件名 + /// + protected abstract string Name { get; } + + /// + /// 文件路径 + /// + protected virtual string BasePath => string.Empty; + + /// + /// 实例映射 + /// + private static readonly Dictionary InstanceMapping = []; + + /// + /// 保存数据 + /// + public void Save() + { + var json = JsonSerializer.Serialize((object)this); + var filePath = Path.Combine(AppContext.BaseDirectory,"dp", BasePath, Name); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); + File.WriteAllText(filePath, json); + } + + /// + /// 加载数据 + /// + /// 文件名 + /// 文件路径 + /// 类型 + /// 实例 + private static T? Load(string name, string? basePath = null) + where T : DpFile + { + var filePath = Path.Combine(AppContext.BaseDirectory, "dp", basePath ?? string.Empty, name); + if (!File.Exists(filePath)) return null; + var json = File.ReadAllText(filePath); + return JsonSerializer.Deserialize(json); + } + + /// + /// 以单例模式加载数据 + /// + /// 类型 + /// 实例 + public static T LoadSingleton() + where T:DpFile + { + // 先从_instanceMapping拿数据 + if (InstanceMapping.TryGetValue(typeof(T), out var cachedInstance)) + { + return (T)cachedInstance; + } + + // 如果缓存没有 则使用找到此DPFile的无参构造函数 使用反射实例化后存放到_instanceMapping + var instance = Activator.CreateInstance(); + // 从instance中获取Name 随后load + InstanceMapping[typeof(T)] = Load(instance.Name, instance.BasePath) ?? instance; + return (T)InstanceMapping[typeof(T)]; + } +} \ No newline at end of file diff --git a/NectarRCON/Dp/RconSettingsDp.cs b/NectarRCON/Dp/RconSettingsDp.cs new file mode 100644 index 0000000..a2bf195 --- /dev/null +++ b/NectarRCON/Dp/RconSettingsDp.cs @@ -0,0 +1,59 @@ +using System.Text; +using System.Text.Json.Serialization; + +namespace NectarRCON.Dp; + +public enum RconEncoding +{ + Utf8 = 0, + Utf16 = 1, + Utf32 = 2, + Gb2312 = 3, + Gbk = 4, + Gb18030 = 5, + Ascii = 6, + Big5 = 7, + HzGb2312 = 8, +} + +public static class RconEncodingExtensions +{ + public static Encoding GetEncoding(this RconEncoding encoding) + => encoding switch + { + RconEncoding.Utf8 => Encoding.UTF8, + RconEncoding.Utf16 => Encoding.GetEncoding("UTF-16"), + RconEncoding.Utf32 => Encoding.UTF32, + RconEncoding.Gb2312 => Encoding.GetEncoding("gb2312"), + RconEncoding.Gbk => Encoding.GetEncoding("gbk"), + RconEncoding.Gb18030 => Encoding.GetEncoding("gb18030"), + RconEncoding.Ascii => Encoding.ASCII, + RconEncoding.Big5 => Encoding.GetEncoding("big5"), + RconEncoding.HzGb2312 => Encoding.GetEncoding("hz-gb-2312"), + _ => Encoding.UTF8, + }; +} + +public class RconSettingsDp : DpFile +{ + protected override string Name => "rcon_settings.json"; + + /// + /// 连接时掉线自动尝试重连 + /// + [JsonPropertyName("auto_reconnect")] + public bool AutoReconnect { get; set; } = true; + + /// + /// 掉线后不关闭连接窗口 + /// + /// + [JsonPropertyName("is_keep_connection_window_open")] + public bool IsKeepConnectionWindowOpen { get; set; } + + /// + /// 文本编码 + /// + [JsonPropertyName("encoding")] + public RconEncoding Encoding { get; set; } = RconEncoding.Utf8; +} \ No newline at end of file diff --git a/NectarRCON/NectarRCON.csproj b/NectarRCON/NectarRCON.csproj index 4f7bd6e..c205304 100644 --- a/NectarRCON/NectarRCON.csproj +++ b/NectarRCON/NectarRCON.csproj @@ -8,6 +8,7 @@ Resources\Icon.ico app.manifest 1.0.0-beta3 + 12 @@ -23,6 +24,7 @@ + diff --git a/NectarRCON/Rcon/RconMultiConnection.cs b/NectarRCON/Rcon/RconMultiConnection.cs index b1bc748..18dc859 100644 --- a/NectarRCON/Rcon/RconMultiConnection.cs +++ b/NectarRCON/Rcon/RconMultiConnection.cs @@ -10,6 +10,7 @@ using System.Net.Sockets; using System.Security.Authentication; using System.Windows.Controls; +using NectarRCON.Dp; namespace NectarRCON.Services { @@ -18,6 +19,7 @@ namespace NectarRCON.Services /// internal class RconMultiConnection : IRconConnection, IDisposable { + private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton(); public event MessageEvent? OnMessage; public event RconEvent? OnClosed; public event RconEvent? OnConnected; @@ -90,6 +92,8 @@ public void Connect() _messageBoxService.Show(ex, $"Server: \"{info.Name}\""); } + //设置编码 + adapter.SetEncoding(_settingsDp.Encoding.GetEncoding()); _connections.Add(info, adapter); } } diff --git a/NectarRCON/Rcon/RconSingleConnection.cs b/NectarRCON/Rcon/RconSingleConnection.cs index 7761f63..95ab132 100644 --- a/NectarRCON/Rcon/RconSingleConnection.cs +++ b/NectarRCON/Rcon/RconSingleConnection.cs @@ -4,12 +4,16 @@ using NectarRCON.Models; using NectarRCON.Rcon; using System; +using System.IO; +using System.Net.Sockets; using System.Security.Authentication; using System.Windows; +using NectarRCON.Dp; namespace NectarRCON.Services; public class RconSingleConnection : IRconConnection { + private readonly RconSettingsDp _settingsDp = DpFile.LoadSingleton(); private readonly IServerPasswordService _serverPasswordService; private readonly ILanguageService _languageService; private readonly IRconConnectionInfoService _rconConnectionInfoService; @@ -67,7 +71,7 @@ public void Connect() // 目前支支持了Minecraft,后期会支持更多(嘛..主要是懒) _rconClient = AdapterHelpers.CreateAdapterInstance(info.Adapter) ?? throw new InvalidOperationException($"adapter not found: {info.Adapter}"); - + _rconClient.SetEncoding(_settingsDp.Encoding.GetEncoding()); string host = address.Split(":")[0]; int port = int.Parse(address.Split(":")[1]); @@ -101,12 +105,37 @@ public void Send(string command) { try { - string result = _rconClient.Run(command) ?? string.Empty; + string result = _rconClient.Run(command); OnMessage?.Invoke(_serverInformation, result); } catch (Exception ex) { Close(); + if (ex is SocketException or IOException && _settingsDp.AutoReconnect) + { + try + { + Connect(); + } + catch + { + // ignored + } + + if (IsConnected()) + { + try + { + string result = _rconClient.Run(command); + OnMessage?.Invoke(_serverInformation, result); + return; + } + catch + { + // ignored + } + } + } MessageBox.Show($"{_languageService.GetKey("text.error")}\n{ex.Message}", ex.GetType().FullName, MessageBoxButton.OK, MessageBoxImage.Error); } } diff --git a/NectarRCON/Resources/Languages/en_us.xaml b/NectarRCON/Resources/Languages/en_us.xaml index b698aec..42c8c56 100644 --- a/NectarRCON/Resources/Languages/en_us.xaml +++ b/NectarRCON/Resources/Languages/en_us.xaml @@ -15,13 +15,18 @@ CheckUpdate RCONManager UI Language + UI Settings UI Theme Dark Theme Light Theme System Theme Rcon passwords... Command Record Limit - + Rcon + Auto Reconnect + KeepConnectionWindowOpen + Encoding + Connect Edit Delete @@ -52,6 +57,11 @@ Run Successful! + + Offline + All clients are offline + Reconnect + Back to Servers Edit diff --git a/NectarRCON/Resources/Languages/zh_cn.xaml b/NectarRCON/Resources/Languages/zh_cn.xaml index 5d5d02a..b56a714 100644 --- a/NectarRCON/Resources/Languages/zh_cn.xaml +++ b/NectarRCON/Resources/Languages/zh_cn.xaml @@ -15,13 +15,18 @@ 检查更新 RCON管理器 UI语言 + 界面设置 UI主题 深色主题 浅色主题 跟随系统 Rcon密码管理 服务器命令回溯记录数 - + Rcon全局设置 + 掉线自动重连 + 掉线不回到主页 + 文本编码 + 连接 编辑 删除 @@ -52,7 +57,12 @@ 执行 执行成功 - + + 已掉线 + 所有客户端都掉线了 + 重新连接 + 回到主页 + 编辑 运行过程中出现异常,应用即将结束。 如果您无法理解以下错误,请在 Github 上提交问题并详细描述发生的情况 diff --git a/NectarRCON/Resources/Languages/zh_tw.xaml b/NectarRCON/Resources/Languages/zh_tw.xaml new file mode 100644 index 0000000..5f0fe90 --- /dev/null +++ b/NectarRCON/Resources/Languages/zh_tw.xaml @@ -0,0 +1,122 @@ + + zh_tw + 繁体中文(zh_tw) + 伺服器 + 伺服器列表 + 伺服器分組 + 日誌 + 清空日誌 + 程式 + 設定 + 關於 + 檢查更新 + RCON管理器 + UI語言 + 界面設定 + UI主題 + 深色主題 + 淺色主題 + 跟隨系統 + Rcon密碼管理 + 伺服器命令回溯記錄數 + Rcon全局設定 + 斷綫自動重連 + 斷線不回到主頁 + 文本編碼 + + 連線 + 編輯 + 刪除 + 密碼 + 伺服器列表 + 新增伺服器 + 輸入伺服器名稱來搜尋... + 此操作將會刪除伺服器! (包括記錄的密碼)\n您確定要刪除嗎? + + 新增伺服器 + 伺服器名稱 + 伺服器地址 + 伺服器端口 + 新增 + 取消 + 相同名稱的伺服器已經存在,請換一個名稱! + 伺服器名稱或伺服器地址不可為空,請更改! + + 爱发电 + MCBBS + Github + + 解析目标地址時遇到錯誤:%s\n請檢查伺服器地址是否合法! + + 編輯密碼 + 密碼 + 無需密碼 + + 執行 + 執行成功 + + 斷線 + 所有客戶端都斷線了 + 重新連接 + 返回伺服器 + + 編輯 + + 執行過程中出現異常,應用即將結束。如果您無法理解以下錯誤,請在 GitHub 上提交問題並詳細描述發生的情況。 + 錯誤 + 警告 + 資訊 + 確定 + 取消 + 不可使用 + 連接 + 移除 + 刪除 + 新增 + + 正在連接伺服器... + 連接伺服器失敗! + 無法連接到遠程伺服器:%s\n請檢查伺服器RCON地址配置是否正確,以及檢查服務端是否開啟了RCON選項! + Rcon密碼錯誤,請檢查您的伺服器密碼是否正確! + + 連接伺服器並認證成功! + 連接已斷開! + 開始連接 + + 無法連接到伺服器,因此無法使用遠程命令功能,請嘗試回到伺服器列表重新連接到本伺服器! + + 無法解析伺服器列表 /servers.json %s\n點選"是"重置伺服器列表(需重新添加伺服器,但記錄的密碼不會消失)\n點選"取消"關閉程式! + 無法解析伺服器列表 /servers.json %s\n點選"是"關閉程式! + 無法儲存檔案 /servers.json\n如果您剛剛進行了添加伺服器操作,請注意,本次儲存並未生效!\n%s + + 無法解析密碼列表 /passwords.json %s\n點選"是"重置密碼列表(需重新設定密碼,但記錄的伺服器不會消失)\n點選"取消"關閉程式! + 無法解析密碼列表 /passwords.json %s\n點選"是"關閉程式! + 無法儲存檔案 /passwords.json\n如果您剛剛進行了設定密碼操作,請注意,本次儲存並未生效!\n%s + + 無法解析配置檔案 /config.json JSON解析失敗:%s\n點選"是"重置配置檔案!\n點選"否"退出程式! + 無法解析配置檔案 /config.json:%s\n點選"是"重置配置檔案!\n點選"否"退出程式! + 無法儲存配置檔案 /config.json:%s\n如果您剛剛更改了配置檔案,請注意,儲存並未生效! + + 讀取和解析組檔案"{0}"時發生錯誤,請嘗試刪除或修復檔案以解決此錯誤! + 相同的 GroupId 已存在(現有值: {0},重複值: {1}) + 相同的 Name 已存在(現有Id: {0},重複Id: {1}) + 檔案中已存在具有相同內部 ID 的分組,請再次嘗試建立 + {0} 檔案名與內部 ID {1} 不匹配 + 相同的組名已存在 + + 點擊「+添加」添加一個分組吧! + 添加分組後,您可以將命令廣播到該分組的所有伺服器 + 該分組沒有任何伺服器,請點擊「+添加」添加一個伺服器吧! + 是否要刪除分組 {0}? + + 新建分組 + 分組名稱 + + 選擇伺服器 + 沒有可用的伺服器 + + 離線 + \ No newline at end of file diff --git a/NectarRCON/Services/LanguageService.cs b/NectarRCON/Services/LanguageService.cs index 2351f22..3d680e9 100644 --- a/NectarRCON/Services/LanguageService.cs +++ b/NectarRCON/Services/LanguageService.cs @@ -57,6 +57,7 @@ public void Refresh() } // 从内部文件加载 _defaultLanguages.Add("zh_cn", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_cn.xaml"); + _defaultLanguages.Add("zh_tw", "pack://application:,,,/NectarRCON;component/Resources/Languages/zh_tw.xaml"); _defaultLanguages.Add("en_us", "pack://application:,,,/NectarRCON;component/Resources/Languages/en_us.xaml"); foreach (KeyValuePair language in _defaultLanguages) { diff --git a/NectarRCON/ViewModels/MainPageViewModel.cs b/NectarRCON/ViewModels/MainPageViewModel.cs index f575a0b..9b0888d 100644 --- a/NectarRCON/ViewModels/MainPageViewModel.cs +++ b/NectarRCON/ViewModels/MainPageViewModel.cs @@ -11,13 +11,16 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Input; +using NectarRCON.Dp; using Wpf.Ui.Mvvm.Contracts; using MessageBox = System.Windows.MessageBox; using TextBox = Wpf.Ui.Controls.TextBox; namespace NectarRCON.ViewModels; + public partial class MainPageViewModel : ObservableObject { + private static readonly RconSettingsDp RconSettings = DpFile.LoadSingleton(); private readonly ILogService _logService; private readonly IServerPasswordService _serverPasswordService; private IRconConnection _rconConnectService; @@ -27,13 +30,14 @@ public partial class MainPageViewModel : ObservableObject private readonly IConnectingDialogService _connectingDialogService; private readonly IMessageBoxService _messageBoxService; - private MainPage? _page = null; - private TextBox? _logTextBox = null; + private MainPage? _page; + private TextBox? _logTextBox; + + [ObservableProperty] private string _commandText = string.Empty; + [ObservableProperty] private string _logText = string.Empty; + [ObservableProperty] private bool _isMultipleConnection; + [ObservableProperty] private bool _isDisconnection; - [ObservableProperty] - private string _commandText = string.Empty; - [ObservableProperty] - private string _logText = string.Empty; public MainPageViewModel() { _logService = App.GetService(); @@ -46,16 +50,19 @@ public MainPageViewModel() WeakReferenceMessenger.Default.Register(this, OnClear); // 选择连接服务 - _rconConnectService = _rconConnectionInfoService.HasMultipleInformation ? - App.GetService(typeof(RconMultiConnection)) : - App.GetService(typeof(RconSingleConnection)); + _rconConnectService = _rconConnectionInfoService.HasMultipleInformation + ? App.GetService(typeof(RconMultiConnection)) + : App.GetService(typeof(RconSingleConnection)); + IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation; } - public void OnClear(object sender, ClearLogValueMessage msg) + + private void OnClear(object sender, ClearLogValueMessage msg) { _logService.Clear(); LogText = string.Empty; } + private void OnMessage(ServerInformation info, string msg) { string logMsg = string.IsNullOrEmpty(msg) @@ -64,32 +71,56 @@ private void OnMessage(ServerInformation info, string msg) LogText += _logService.Log($"{info.Name}:" + logMsg); _logTextBox?.ScrollToEnd(); } + private void OnClosed(ServerInformation info) { LogText += _logService.Log($"{info.Name}\t{_languageService.GetKey("text.server.closed")}"); + IsDisconnection = !_rconConnectService.IsConnected(); + } + + [RelayCommand] + private async void Load(RoutedEventArgs e) + { + // GetLogs + LogText = string.Empty; + LogText = _logService.GetText(); + + _page = e.Source as MainPage; + await ConnectAsync(); } + [RelayCommand] - public async void Load(RoutedEventArgs e) + private async void ReConnect() + { + if (_rconConnectService.IsConnected()) + _rconConnectService.Close(); + IsDisconnection = false; + await ConnectAsync(); + } + + private async Task ConnectAsync() { + IsMultipleConnection = _rconConnectionInfoService.HasMultipleInformation; + _rconConnectService.OnConnected -= OnConnected; + _rconConnectService.OnMessage -= OnMessage; + _rconConnectService.OnClosed -= OnClosed; + await Task.CompletedTask; try { _connectingDialogService.Show(); // 选择连接服务 - _rconConnectService = _rconConnectionInfoService.HasMultipleInformation ? - App.GetService(typeof(RconMultiConnection)) : - App.GetService(typeof(RconSingleConnection)); + _rconConnectService = _rconConnectionInfoService.HasMultipleInformation + ? App.GetService(typeof(RconMultiConnection)) + : App.GetService(typeof(RconSingleConnection)); - WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage() + WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage { IsLoaded = true, }); - _page = e.Source as MainPage; _logTextBox = (TextBox)LogicalTreeHelper.FindLogicalNode(_page, "LogText"); - - LogText = string.Empty; - LogText = _logService.GetText(); LogText += _logService.Log(_languageService.GetKey("text.server.start")); + _logTextBox?.ScrollToEnd(); _rconConnectService.OnConnected += OnConnected; _rconConnectService.OnMessage += OnMessage; _rconConnectService.OnClosed += OnClosed; @@ -97,22 +128,26 @@ public async void Load(RoutedEventArgs e) } catch (SocketException ex) { - _messageBoxService.Show(_languageService.GetKey("text.server.connect.fail.text") + var msg = _languageService.GetKey("text.server.connect.fail.text") .Replace("\\n", "\n") - .Replace("%s", ex.Message), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); + .Replace("%s", ex.Message); + _messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK, + MessageBoxImage.Error); + LogText += _logService.Log(msg); + _logTextBox?.ScrollToEnd(); } catch (AuthenticationException ex) { - _messageBoxService.Show(ex.Message + _languageService.GetKey("text.server.connect.auth_fail") - .Replace("\\n", "\n"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); - if (_rconConnectionInfoService.HasMultipleInformation) - { - _navigationService.Navigate(typeof(GroupPage)); - } - else - { - _navigationService.Navigate(typeof(ServersPage)); - } + var msg = ex.Message + _languageService.GetKey("text.server.connect.auth_fail") + .Replace("\\n", "\n"); + _messageBoxService.Show(msg, _languageService.GetKey("text.error"), MessageBoxButton.OK, + MessageBoxImage.Error); + LogText += _logService.Log(msg); + + // 如果认证失败 就根据当前模式返回对应页面 + _navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation + ? typeof(GroupPage) + : typeof(ServersPage)); } finally { @@ -121,19 +156,30 @@ public async void Load(RoutedEventArgs e) // 当只有一个服务器时IsConnected会返回单个客户端的连接状态 // 当有多个服务器时只要有一个客户端在线,IsConnected就会返回True - if (!_rconConnectService.IsConnected()) + if (!_rconConnectService.IsConnected() && !RconSettings.IsKeepConnectionWindowOpen) { _navigationService.Navigate(2); } + + IsDisconnection = !_rconConnectService.IsConnected(); } private void OnConnected(ServerInformation info) { LogText += _logService.Log($"$ {info.Name}\t{_languageService.GetKey("text.server.connected")}"); + IsDisconnection = false; } [RelayCommand] - public void Exit() + private void BackHome() + { + _navigationService.Navigate(_rconConnectionInfoService.HasMultipleInformation + ? typeof(GroupPage) + : typeof(ServersPage)); + } + + [RelayCommand] + private void Exit() { WeakReferenceMessenger.Default.Send(new MainPageLoadValueMessage() { @@ -143,9 +189,12 @@ public void Exit() _rconConnectService.Close(); _rconConnectService.OnMessage -= OnMessage; _rconConnectService.OnClosed -= OnClosed; + _rconConnectService.OnConnected -= OnConnected; + IsDisconnection = false; // 重置状态 此时没有任何连接 } + [RelayCommand] - public void Run() + private void Run() { if (_rconConnectService.IsConnected()) { @@ -156,12 +205,15 @@ public void Run() } else { + IsDisconnection = true; _rconConnectService.Close(); - MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"), _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); + MessageBox.Show(_languageService.GetKey("text.server.not_connect.text"), + _languageService.GetKey("text.error"), MessageBoxButton.OK, MessageBoxImage.Error); } } + [RelayCommand] - public void KeyDown(KeyEventArgs e) + private void KeyDown(KeyEventArgs e) { var textBox = (System.Windows.Controls.TextBox)e.Source; _commandText = textBox.Text; diff --git a/NectarRCON/ViewModels/SettingPageViewModel.cs b/NectarRCON/ViewModels/SettingPageViewModel.cs index 1d1609a..0a19b0f 100644 --- a/NectarRCON/ViewModels/SettingPageViewModel.cs +++ b/NectarRCON/ViewModels/SettingPageViewModel.cs @@ -1,4 +1,5 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NectarRCON.Core.Helper; using NectarRCON.Interfaces; @@ -8,6 +9,7 @@ using System.Linq; using System.Windows; using System.Windows.Controls; +using NectarRCON.Dp; using Wpf.Ui.Mvvm.Contracts; namespace NectarRCON.ViewModels; @@ -17,20 +19,64 @@ public partial class SettingPageViewModel : ObservableObject private readonly ILanguageService _languageService; private readonly IConfigService _configService; private readonly IThemeService _themeService; + private readonly RconSettingsDp _rconSettingsDp = DpFile.LoadSingleton(); [ObservableProperty] private int _languageSelectedIndex = -1; [ObservableProperty] private int _themeSelectedIndex = -1; + + [ObservableProperty] + private bool _rconAutoReconnect; + + [ObservableProperty] + private bool _isKeepConnectionWindowOpen; + + [ObservableProperty] + private ObservableCollection _rconEncoding = []; + + [ObservableProperty] + private string _selectedRconEncoding; [ObservableProperty] - private ObservableCollection _languages = new(); + private ObservableCollection _languages = []; + public SettingPageViewModel() { _languageService = App.GetService(); _configService = App.GetService(); _themeService = App.GetService(); + + RconAutoReconnect = _rconSettingsDp.AutoReconnect; + IsKeepConnectionWindowOpen = _rconSettingsDp.IsKeepConnectionWindowOpen; + + RconEncoding.Clear(); + foreach (var encoding in Enum.GetNames(typeof(RconEncoding))) + { + RconEncoding.Add(encoding); + } + + SelectedRconEncoding = _rconSettingsDp.Encoding.ToString(); + } + + partial void OnRconAutoReconnectChanged(bool value) + { + _rconSettingsDp.AutoReconnect = value; + _rconSettingsDp.Save(); + } + + partial void OnIsKeepConnectionWindowOpenChanged(bool value) + { + _rconSettingsDp.IsKeepConnectionWindowOpen = value; + _rconSettingsDp.Save(); } + + partial void OnSelectedRconEncodingChanged(string value) + { + _rconSettingsDp.Encoding = Enum.GetValues().FirstOrDefault(e => e.ToString() == value); + _rconSettingsDp.Save(); + } + [RelayCommand] public void PageLoad(RoutedEventArgs e) { diff --git a/NectarRCON/Views/Pages/MainPage.xaml b/NectarRCON/Views/Pages/MainPage.xaml index feaf82f..9d4dcb2 100644 --- a/NectarRCON/Views/Pages/MainPage.xaml +++ b/NectarRCON/Views/Pages/MainPage.xaml @@ -13,6 +13,9 @@ + + + @@ -78,5 +81,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +