CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
398 lines
14 KiB
C#
398 lines
14 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using PCL.Core.App;
|
|
using PCL.Core.App.Localization;
|
|
using PCL.Core.Link.EasyTier;
|
|
using PCL.Core.Link.Lobby;
|
|
using PCL.Core.Link.McPing;
|
|
using PCL.Core.Link.McPing.Model;
|
|
using PCL.Core.Link.Natayark;
|
|
using PCL.Core.Logging;
|
|
using PCL.Core.UI;
|
|
using PCL.Core.Utils.OS;
|
|
using PCL.Network;
|
|
using PCL.Network.Loaders;
|
|
|
|
namespace PCL;
|
|
|
|
public static class ModLink
|
|
{
|
|
#region 大厅操作
|
|
|
|
public static bool LobbyPrecheck()
|
|
{
|
|
if (!LobbyInfoProvider.IsLobbyAvailable)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.LobbyUnavailable"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
if (ModProfile.selectedProfile is not null)
|
|
if (ModProfile.selectedProfile.Username.Contains("|"))
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.InvalidPlayerId"));
|
|
return false;
|
|
}
|
|
|
|
if (LobbyInfoProvider.RequiresLogin)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(States.Link.NaidRefreshToken))
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.LoginFirst"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
NatayarkProfileManager.GetNaidDataAsync((string)States.Link.NaidRefreshToken, true)
|
|
.GetAwaiter().GetResult();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log("[Link] 刷新 Natayark ID 信息失败,需要重新登录");
|
|
HintService.Hint(Lang.Text("Link.Mod.ReLoginRequired"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
var waitCount = 0;
|
|
while (string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username))
|
|
{
|
|
if (waitCount > 30)
|
|
break;
|
|
Thread.Sleep(500);
|
|
waitCount += 1;
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username))
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.NaidFetchFailed"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
if (LobbyInfoProvider.RequiresRealName && !NatayarkProfileManager.NaidProfile.IsRealNamed)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.RealNameRequired"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
if (NatayarkProfileManager.NaidProfile.Status != 0)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.AccountBanned"), HintType.Error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(Config.Link.Username) && string.IsNullOrWhiteSpace(NatayarkProfileManager.NaidProfile.Username))
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.UsernameOrLogin"), HintType.Error);
|
|
return false;
|
|
}
|
|
|
|
if (ETController.Precheck() == 1)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.DownloadingDeps"));
|
|
DownloadEasyTier();
|
|
return false;
|
|
}
|
|
|
|
if (dlEasyTierLoader is not null)
|
|
{
|
|
if (dlEasyTierLoader.State == ModBase.LoadState.Loading)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.EasyTierNotReady"));
|
|
return false;
|
|
}
|
|
|
|
if (dlEasyTierLoader.State == ModBase.LoadState.Failed ||
|
|
dlEasyTierLoader.State == ModBase.LoadState.Aborted)
|
|
{
|
|
HintService.Hint(Lang.Text("Link.Mod.DownloadingEasyTier"));
|
|
DownloadEasyTier();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 端口查找
|
|
|
|
public class PortFinder
|
|
{
|
|
[DllImport("iphlpapi.dll", SetLastError = true)]
|
|
public static extern int GetExtendedTcpTable(nint pTcpTable, ref int dwOutBufLen, bool bOrder, int ulAf,
|
|
int tableClass, int reserved);
|
|
|
|
public static List<int> GetProcessPort(int dwProcessId)
|
|
{
|
|
var ports = new List<int>();
|
|
var tcpTable = nint.Zero;
|
|
var dwSize = 0;
|
|
int dwRetVal;
|
|
|
|
if (dwProcessId == 0) return ports;
|
|
|
|
dwRetVal = GetExtendedTcpTable(nint.Zero, ref dwSize, true, 2, 3, 0);
|
|
if (dwRetVal != 0 && dwRetVal != 122) // 122 表示缓冲区不足
|
|
return ports;
|
|
|
|
tcpTable = Marshal.AllocHGlobal(dwSize);
|
|
try
|
|
{
|
|
if (GetExtendedTcpTable(tcpTable, ref dwSize, true, 2, 3, 0) != 0) return ports;
|
|
|
|
var tablePtr = tcpTable;
|
|
var dwNumEntries = Marshal.ReadInt32(tablePtr);
|
|
tablePtr = nint.Add(tablePtr, 4);
|
|
|
|
for (int i = 0, loopTo = dwNumEntries - 1; i <= loopTo; i++)
|
|
{
|
|
var row = Marshal.PtrToStructure<MIB_TCPROW_OWNER_PID>(tablePtr);
|
|
if (row.dwOwningPid == dwProcessId)
|
|
ports.Add((row.dwLocalPort >> 8) | ((row.dwLocalPort & 0xFF) << 8)); // 转换端口号
|
|
tablePtr = nint.Add(tablePtr, Marshal.SizeOf<MIB_TCPROW_OWNER_PID>());
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
Marshal.FreeHGlobal(tcpTable);
|
|
}
|
|
|
|
return ports;
|
|
}
|
|
|
|
// 定义需要的结构和常量
|
|
[StructLayout(LayoutKind.Sequential)]
|
|
public struct MIB_TCPROW_OWNER_PID
|
|
{
|
|
public int dwState;
|
|
public int dwLocalAddr;
|
|
public int dwLocalPort;
|
|
public int dwRemoteAddr;
|
|
public int dwRemotePort;
|
|
public int dwOwningPid;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Minecraft 实例探测
|
|
|
|
public static async Task<List<Tuple<int, McPingResult, string>>> MCInstanceFindingAsync()
|
|
{
|
|
// Java 进程 PID 查询
|
|
var pIDLookupResult = new List<string>();
|
|
var javaNames = new List<string>();
|
|
javaNames.Add("java");
|
|
javaNames.Add("javaw");
|
|
|
|
foreach (var TargetJava in javaNames)
|
|
{
|
|
var javaProcesses = Process.GetProcessesByName(TargetJava);
|
|
ModBase.Log($"[MCDetect] 找到 {TargetJava} 进程 {javaProcesses.Length} 个");
|
|
|
|
if (javaProcesses is null || javaProcesses.Length == 0)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
foreach (var p in javaProcesses)
|
|
{
|
|
ModBase.Log("[MCDetect] 检测到 Java 进程,PID: " + p.Id);
|
|
pIDLookupResult.Add(p.Id.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
var res = new List<Tuple<int, McPingResult, string>>();
|
|
try
|
|
{
|
|
if (pIDLookupResult.Count == 0)
|
|
return res;
|
|
var lookupList = new List<Tuple<int, int>>();
|
|
foreach (var pid in pIDLookupResult)
|
|
{
|
|
var infos = new List<Tuple<int, int>>();
|
|
var ports = PortFinder.GetProcessPort(int.Parse(pid));
|
|
foreach (var port in ports)
|
|
infos.Add(new Tuple<int, int>(port, int.Parse(pid)));
|
|
lookupList.AddRange(infos);
|
|
}
|
|
|
|
ModBase.Log($"[MCDetect] 获取到端口数量 {lookupList.Count}");
|
|
// 并行查找本地,超时 3s 自动放弃
|
|
var checkTasks = lookupList.Select(
|
|
lookup => Task.Run(async () =>
|
|
{
|
|
ModBase.Log($"[MCDetect] 找到疑似端口,开始验证:{lookup}");
|
|
using (var test = McPingServiceFactory.CreateService("127.0.0.1", lookup.Item1, 3000))
|
|
{
|
|
try
|
|
{
|
|
var info = await test.PingAsync();
|
|
var launcher = GetLauncherBrand(lookup.Item2);
|
|
if (!string.IsNullOrWhiteSpace(info?.Version.Name))
|
|
{
|
|
ModBase.Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界");
|
|
res.Add(new Tuple<int, McPingResult, string>(lookup.Item1, info, launcher));
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.InnerException is ObjectDisposedException)
|
|
{
|
|
ModBase.Log($"[McDetect] {lookup} 验证超时,已强制断开连接,将尝试旧版检测");
|
|
}
|
|
else
|
|
{
|
|
ModBase.Log(ex, $"[McDetect] {lookup} 验证出错,将尝试旧版检测");
|
|
}
|
|
}
|
|
}
|
|
using (var test = McPingServiceFactory.CreateLegacyService("127.0.0.1", lookup.Item1, 3000))
|
|
{
|
|
try
|
|
{
|
|
var info = await test.PingAsync();
|
|
if (!string.IsNullOrWhiteSpace(info?.Version.Name))
|
|
{
|
|
ModBase.Log($"[MCDetect] 端口 {lookup} 为有效 Minecraft 世界");
|
|
res.Add(new Tuple<int, McPingResult, string>(lookup.Item1, info, string.Empty));
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (ex.InnerException is ObjectDisposedException)
|
|
{
|
|
ModBase.Log($"[McDetect] {lookup} 验证超时,已强制断开连接");
|
|
}
|
|
else
|
|
{
|
|
ModBase.Log(ex, $"[McDetect] {lookup} 验证出错");
|
|
}
|
|
}
|
|
}
|
|
return Task.CompletedTask;
|
|
})).ToArray();
|
|
await Task.WhenAll(checkTasks);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, "[MCDetect] 获取端口信息错误");
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
public static string GetLauncherBrand(int pid)
|
|
{
|
|
try
|
|
{
|
|
var cmd = ProcessInterop.GetCommandLine(pid);
|
|
if (cmd.Contains("-Dminecraft.launcher.brand="))
|
|
return cmd.AfterFirst("-Dminecraft.launcher.brand=").BeforeFirst("-").TrimEnd('\'', ' ');
|
|
|
|
return cmd.AfterFirst("--versionType ").BeforeFirst("-").TrimEnd('\'', ' ');
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, $"[MCDetect] 检测 PID {pid} 进程的启动参数失败");
|
|
return "";
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region EasyTier
|
|
|
|
public static ModLoader.LoaderCombo<JsonObject> dlEasyTierLoader;
|
|
|
|
public static int DownloadEasyTier()
|
|
{
|
|
var dlTargetPath = $"{ModBase.pathTemp}EasyTier\\EasyTier-{ETInfoProvider.ETVersion}.zip";
|
|
|
|
Basics.RunInNewThread(() =>
|
|
{
|
|
try
|
|
{
|
|
// Initialize loaders
|
|
var loaders = new List<ModLoader.LoaderBase>();
|
|
|
|
// Setup download addresses
|
|
var architecture = SystemInfo.IsArm64System ? "arm64" : "x86_64";
|
|
var addresses = new List<string>
|
|
{
|
|
$"https://staticassets.naids.com/resources/pclce/static/easytier/easytier-windows-{architecture}-v{ETInfoProvider.ETVersion}.zip",
|
|
$"https://s3.pysio.online/pcl2-ce/static/easytier/easytier-windows-{architecture}-v{ETInfoProvider.ETVersion}.zip"
|
|
};
|
|
|
|
// 1. Download EasyTier
|
|
loaders.Add(new LoaderDownload(Lang.Text("Link.Mod.Task.DownloadEasyTier"), new List<DownloadFile>
|
|
{
|
|
new(addresses.ToArray(), dlTargetPath, new ModBase.FileChecker(1024 * 64))
|
|
}) { ProgressWeight = 15 });
|
|
|
|
// 2. Extract files
|
|
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Link.Mod.Task.ExtractFiles"), _ =>
|
|
ModBase.ExtractFile(dlTargetPath,
|
|
Path.Combine(Paths.SharedLocalData, "EasyTier", ETInfoProvider.ETVersion))
|
|
) { block = true });
|
|
|
|
// 3. Cleanup
|
|
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Link.Mod.Task.CleanCache"), _ =>
|
|
{
|
|
File.Delete(dlTargetPath);
|
|
CleanupEasyTierCache();
|
|
}));
|
|
|
|
// 4. Update UI hint
|
|
loaders.Add(new ModLoader.LoaderTask<int, int>(Lang.Text("Link.Mod.Task.RefreshUi"), _ =>
|
|
HintWrapper.Show(Lang.Text("Link.Mod.DownloadComplete"), HintTheme.Success)
|
|
) { show = false });
|
|
|
|
// Start loader combo
|
|
dlEasyTierLoader = new ModLoader.LoaderCombo<JsonObject>(Lang.Text("Link.Mod.Task.InitLobby"), loaders);
|
|
dlEasyTierLoader.Start();
|
|
|
|
// Taskbar and UI notification
|
|
ModLoader.LoaderTaskbarAdd(dlEasyTierLoader);
|
|
ModMain.frmMain.BtnExtraDownload.ShowRefresh();
|
|
ModMain.frmMain.BtnExtraDownload.Ribble();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Error handling with concise English logs
|
|
LogWrapper.Warn(ex, "Failed to download EasyTier dependency files");
|
|
HintWrapper.Show(Lang.Text("Link.Mod.DownloadEasyTierFailed"), HintTheme.Error);
|
|
}
|
|
});
|
|
|
|
return 0;
|
|
}
|
|
|
|
private static void CleanupEasyTierCache()
|
|
{
|
|
var subDirs = Directory.GetDirectories(Path.Combine(Paths.SharedLocalData, "EasyTier"));
|
|
foreach (var folderPath in subDirs)
|
|
{
|
|
var name = Path.GetFileName(folderPath);
|
|
if (!name.Equals(ETInfoProvider.ETVersion))
|
|
try
|
|
{
|
|
Directory.Delete(folderPath, true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
ModBase.Log(ex, "[Link] 清理旧版本 EasyTier 出错");
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|