CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
498 lines
16 KiB
C#
498 lines
16 KiB
C#
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<MinecraftServerInfo> serverList = new();
|
|
private static readonly List<ServerCard> 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<NbtList>(
|
|
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<NbtList>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 刷新服务器列表
|
|
/// </summary>
|
|
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<NbtList>(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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从servers.dat文件读取服务器信息
|
|
/// </summary>
|
|
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<NbtList>(serversFile, "servers");
|
|
ParseServersFromNBT(nbtData);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, Lang.Text("Instance.Server.ReadFileFailed"));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析NBT格式的服务器数据
|
|
/// </summary>
|
|
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<NbtString>("ip")?.Value ?? "Unknown";
|
|
var name = server.Get<NbtString>("name")?.Value ?? "Unknown";
|
|
var iconBase64 = server.Get<NbtString>("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.");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 更新服务器UI显示
|
|
/// </summary>
|
|
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<Task>();
|
|
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 失败");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// ping单个服务器
|
|
/// </summary>
|
|
public static async Task<MinecraftServerInfo> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Minecraft服务器信息类
|
|
/// </summary>
|
|
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; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// 服务器状态枚举
|
|
/// </summary>
|
|
public enum ServerStatus
|
|
{
|
|
Unknown,
|
|
Online,
|
|
Offline,
|
|
Pinging
|
|
} |