using System.Collections.ObjectModel; using System.IO; using System.Windows; using System.Windows.Input; using FluentValidation; using fNbt; using PCL.Core.Link.McPing; using PCL.Core.Link.McPing.Model; using PCL.Core.Minecraft; using PCL.Core.Utils.Validate; using PCL.Core.App.Localization; namespace PCL; public partial class PageInstanceServer : MyPageRight { private const int debounceInterval = 2000; public static readonly List serverList = new(); private static readonly List serverCardList = new(); private CancellationTokenSource _cts; private DateTime _lastRefresh = DateTime.MinValue; public PageInstanceServer() { InitializeComponent(); Loaded += PageLoaded; IsVisibleChanged += PageInstanceServer_IsVisibleChanged; } private async void PageLoaded(object e, RoutedEventArgs sender) { serverList.Clear(); serverCardList.Clear(); PanServers.Children.Clear(); await LoadServersFromFileAsync(); RefreshTip(); foreach (var server in serverList) { var serverCard = new ServerCard(); serverCard.RemoveServer += RemoveServerEvent; serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b); serverCard.UpdateServerInfo(server); serverCardList.Add(serverCard); PanServers.Children.Add(serverCard); } PingAllServers(); } private void PageInstanceServer_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { if (!IsVisible) if (_cts is not null) { _cts.Cancel(); _cts.Dispose(); // 清理旧的 CancellationTokenSource _cts = null; } } private async void RemoveServerEvent(object sender, EventArgs e) { // Get server index var index = PanServers.Children.IndexOf((UIElement)sender); if (index < 0) { HintService.Hint(Lang.Text("Instance.Server.IndexNotFound"), HintType.Error); return; } // Read NBT file var nbtData = await NbtFileHandler.ReadTagInNbtFileAsync( Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"), "servers"); if (nbtData is null) { HintService.Hint(Lang.Text("Instance.Server.ReadDataFailed"), HintType.Error); return; } // Remove server from NBT data nbtData.RemoveAt(index); var clonedNbtData = (NbtList)nbtData.Clone(); // Write back to NBT file if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"))) { HintService.Hint(Lang.Text("Instance.Server.WriteDataFailed"), HintType.Error); return; } // Remove server from list and UI serverList.RemoveAt(index); serverCardList.Remove((ServerCard)sender); if (serverList.Count == 0) RefreshTip(); // Remove UI element PanServers.Children.Remove((UIElement)sender); // Success message HintService.Hint(Lang.Text("Instance.Server.Removed"), HintType.Success); } private async void EditServer(object sender, ServerCard.ResultEventArgs e) { // Read NBT file var nbtData = await NbtFileHandler.ReadTagInNbtFileAsync(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"), "servers"); if (nbtData is null) { HintService.Hint(Lang.Text("Instance.Server.ReadDataFailed"), HintType.Error); return; } // Get server index var index = PanServers.Children.IndexOf((UIElement)sender); if (index < 0 || index >= nbtData.Count) { HintService.Hint(Lang.Text("Instance.Server.IndexNotFound"), HintType.Error); return; } // Verify server data var server = nbtData[index] as NbtCompound; // Update server data server["name"] = new NbtString("name", e.Param1); server["ip"] = new NbtString("ip", e.Param2); // Write updated NBT data var clonedNbtData = (NbtList)nbtData.Clone(); if (!await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"))) { HintService.Hint(Lang.Text("Instance.Server.WriteDataFailed"), HintType.Error); return; } var serverCard = sender as ServerCard; serverCard.server.Name = e.Param1; serverCard.server.Address = e.Param2; await serverCard.RefreshServerStatusAsync(true); // Success message HintService.Hint(Lang.Text("Instance.Server.Updated"), HintType.Success); } /// /// 刷新服务器列表 /// public async void RefreshServers() { ModBase.Log("刷新服务器列表"); try { // 读取服务器信息 await LoadServersFromFileAsync(); // 在UI线程中更新界面 ModBase.RunInUi(() => UpdateServerUi()); // 异步ping所有服务器 PingAllServers(); } catch (Exception ex) { ModBase.Log( ex, Lang.Text("Instance.Server.RefreshFailed"), ModBase.LogLevel.Feedback, userSummary: Lang.Text("Instance.Server.RefreshFailed")); ModBase.RunInUi(() => HintService.Hint( Lang.Text("Instance.Server.RefreshFailed.WithDetail", ex.ToString()), HintType.Error)); } } private void BtnRefresh_Click(object sender, MouseButtonEventArgs e) { if ((DateTime.Now - _lastRefresh).TotalMilliseconds < debounceInterval) { HintService.Hint(Lang.Text("Instance.Server.NoFrequentRefresh")); return; } _lastRefresh = DateTime.Now; HintService.Hint(Lang.Text("Instance.Server.RefreshingList")); try { RefreshServers(); } catch (Exception ex) { ModBase.Log( ex, Lang.Text("Instance.Server.RefreshFailed"), ModBase.LogLevel.Feedback, userSummary: Lang.Text("Instance.Server.RefreshFailed")); HintService.Hint( Lang.Text("Instance.Server.RefreshFailed.WithDetail", ex.ToString()), HintType.Error); } } private async void BtnAddServer_Click(object sender, MouseButtonEventArgs e) { var result = GetServerInfo(new MinecraftServerInfo { Name = Lang.Text("Instance.Server.DefaultName"), Address = "" }); if (result.Success) { var newServer = new MinecraftServerInfo { Name = result.Name, Address = result.Address, Status = ServerStatus.Unknown }; serverList.Add(newServer); RefreshTip(); var serverCard = new ServerCard(); serverCard.RemoveServer += RemoveServerEvent; serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b); serverCard.UpdateServerInfo(newServer); serverCardList.Add(serverCard); PanServers.Children.Add(serverCard); await serverCard.RefreshServerStatusAsync(false); var serversDatPath = Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"); NbtList nbtData; if (!File.Exists(serversDatPath)) { nbtData = new NbtList("servers", NbtTagType.Compound); RefreshTip(); } else { nbtData = await NbtFileHandler.ReadTagInNbtFileAsync(serversDatPath, "servers"); } if (nbtData is not null) { var server = new NbtCompound(); server["name"] = new NbtString("name", result.Name); server["ip"] = new NbtString("ip", result.Address); if (nbtData.Count == 0) nbtData.ListType = NbtTagType.Compound; nbtData.Add(server); var clonedNbtData = (NbtList)nbtData.Clone(); await NbtFileHandler.WriteTagInNbtFileAsync(clonedNbtData, serversDatPath); } } } public static (string Name, string Address, bool Success) GetServerInfo(MinecraftServerInfo server) { var newName = ModMain.MyMsgBoxInput(Lang.Text("Instance.Server.EditTitle"), Lang.Text("Instance.Server.NamePrompt"), server.Name, [new NullOrWhiteSpaceValidator()]); if (string.IsNullOrEmpty(newName)) return (string.Empty, string.Empty, false); var newAddress = ModMain.MyMsgBoxInput(Lang.Text("Instance.Server.EditTitle"), Lang.Text("Instance.Server.AddressPrompt"), server.Address, [new NullOrWhiteSpaceValidator()]); if (string.IsNullOrEmpty(newAddress)) return (string.Empty, string.Empty, false); return (newName, newAddress, true); } /// /// 从servers.dat文件读取服务器信息 /// private async Task LoadServersFromFileAsync() { serverList.Clear(); var serversFile = Path.Combine(PageInstanceLeft.McInstance.PathIndie, "servers.dat"); if (!File.Exists(serversFile)) return; try { // 读取NBT格式的servers.dat文件 var nbtData = await NbtFileHandler.ReadTagInNbtFileAsync(serversFile, "servers"); ParseServersFromNBT(nbtData); } catch (Exception ex) { ModBase.Log(ex, Lang.Text("Instance.Server.ReadFileFailed")); } } /// /// 解析NBT格式的服务器数据 /// private void ParseServersFromNBT(NbtList serversList) { if (serversList is not null) { ModBase.Log($"Found {serversList.Count} servers:"); // 遍历 servers 列表中的每个服务器 for (int i = 0, loopTo = serversList.Count - 1; i <= loopTo; i++) { var server = serversList[i] as NbtCompound; if (server is not null) { // 提取服务器信息 // Dim hidden As Byte = If(server.Get(Of NbtByte)("hidden")?.Value, 0) var ip = server.Get("ip")?.Value ?? "Unknown"; var name = server.Get("name")?.Value ?? "Unknown"; var iconBase64 = server.Get("icon")?.Value; ModBase.Log($"服务器 {i + 1}:"); ModBase.Log($" 名字: {name}"); ModBase.Log($" IP: {ip}"); // Log($" Hidden: {If(hidden = 1, "Yes", "No")}") serverList.Add(new MinecraftServerInfo { Name = name, Address = ip, Status = ServerStatus.Unknown, Icon = iconBase64 }); } } } else { ModBase.Log("No 'servers' list found in servers.dat."); } } /// /// 更新服务器UI显示 /// private void UpdateServerUi() { PanServers.Children.Clear(); RefreshTip(); foreach (var server in serverList) { var serverCard = new ServerCard(); serverCard.RemoveServer += RemoveServerEvent; serverCard.EditServer += (a, b) => this.EditServer(a, (ServerCard.ResultEventArgs)b); serverCard.UpdateServerInfo(server); serverCardList.Add(serverCard); PanServers.Children.Add(serverCard); } } private void RefreshTip() { if (serverList.Count == 0) { ModBase.Log(Lang.Text("Instance.Server.NoServersFound")); PanNoServer.Visibility = Visibility.Visible; PanContent.Visibility = Visibility.Collapsed; PanServers.Visibility = Visibility.Collapsed; return; } ModBase.Log(Lang.Text("Instance.Server.FoundServers")); PanNoServer.Visibility = Visibility.Collapsed; PanContent.Visibility = Visibility.Visible; PanServers.Visibility = Visibility.Visible; } private async void PingAllServers() { if (_cts is not null) { _cts.Cancel(); _cts.Dispose(); } _cts = new CancellationTokenSource(); var token = _cts.Token; var semaphore = new SemaphoreSlim(5); // 限制最多 5 个并发任务 var tasks = new List(); try { var snapshot = serverCardList.ToList(); foreach (var server in snapshot) { var currentServer = server; await semaphore.WaitAsync(token); tasks.Add(Task.Run(async () => { try { await currentServer.RefreshServerStatusAsync(false, token); } catch (Exception ex) { ModBase.Log(ex, $"Ping 服务器失败: {currentServer}"); } finally { semaphore.Release(); } }, token)); } await Task.WhenAll(tasks); // 等待所有任务完成 } catch (OperationCanceledException ex) { ModBase.Log("PingAllServers 被取消", ModBase.LogLevel.Debug); } catch (Exception ex) { ModBase.Log(ex, "PingAllServers 失败"); } } /// /// ping单个服务器 /// public static async Task PingServerAsync(MinecraftServerInfo server, CancellationToken token) { try { var addr = await ServerAddressResolver.GetResolvedServerAddressAsync(server.Address, token); using (var query = McPingServiceFactory.CreateService(addr.Host, addr.Ip, addr.Port)) { McPingResult? result; ModBase.Log("Pinging server: " + server.Address + ":" + addr.Port); result = await query.PingAsync(token); // 传递 token ModBase.Log("Ping result: " + (result is not null ? "Success" : "Failed")); if (result is not null) { server.Status = ServerStatus.Online; server.PlayerCount = result.Players.Online; server.MaxPlayers = result.Players.Max; server.Description = result.Description ?? string.Empty; server.Version = result.Version.Name; server.Ping = (int)result.Latency; server.Icon = result.Favicon; } else { server.Status = ServerStatus.Offline; } } } catch (OperationCanceledException ex) { server.Status = ServerStatus.Offline; ModBase.Log("Ping 服务器被取消: " + server.Address, ModBase.LogLevel.Debug); } catch (Exception ex) { server.Status = ServerStatus.Offline; ModBase.Log(ex, $"Ping 服务器失败: {server.Address}:{server.Port}"); } return server; } } /// /// Minecraft服务器信息类 /// public class MinecraftServerInfo { public string Name { get; set; } public string Address { get; set; } public int Port { get; set; } = 25565; public ServerStatus Status { get; set; } = ServerStatus.Unknown; public int PlayerCount { get; set; } public int MaxPlayers { get; set; } public string Description { get; set; } = ""; public string Version { get; set; } = ""; public int Ping { get; set; } public string Icon { get; set; } = ""; } /// /// 服务器状态枚举 /// public enum ServerStatus { Unknown, Online, Offline, Pinging }