CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
488 lines
20 KiB
C#
488 lines
20 KiB
C#
using System.IO;
|
||
using System.Text.Json;
|
||
using PCL.Core.App;
|
||
using PCL.Core.IO;
|
||
using PCL.Core.Minecraft;
|
||
using PCL.Core.Minecraft.Java.UserPreference;
|
||
using PCL.Network;
|
||
using PCL.Network.Loaders;
|
||
using PCL.Core.App.Localization;
|
||
using PCL.Core.Utils.OS;
|
||
using PCL.Core.Utils;
|
||
|
||
namespace PCL;
|
||
|
||
public static class ModJava
|
||
{
|
||
public static int javaListCacheVersion = 7;
|
||
|
||
/// <summary>
|
||
/// 防止多个需要 Java 的部分同时要求下载 Java(#3797)。
|
||
/// </summary>
|
||
public static object javaLock = new();
|
||
|
||
/// <summary>
|
||
/// 目前所有可用的 Java。
|
||
/// </summary>
|
||
public static JavaManager Javas => JavaService.JavaManager;
|
||
|
||
/// <summary>
|
||
/// 根据要求返回最适合的 Java,若找不到则返回 Nothing。
|
||
/// 最小与最大版本在与输入相同时也会通过。
|
||
/// 必须在工作线程调用,且必须包括 SyncLock JavaLock。
|
||
/// </summary>
|
||
public static JavaEntry JavaSelect(string cancelException, Version minVersion = null, Version maxVersion = null,
|
||
McInstance relatedInstance = null, bool enforceVersionRange = false)
|
||
{
|
||
ModBase.Log(
|
||
$"[Java] 要求选择合适 Java,要求最低版本 {(minVersion is not null ? minVersion.ToString() : "未指定")},要求选择的最高版本 {(maxVersion is not null ? maxVersion.ToString() : "未指定")},关联实例 {(relatedInstance is not null ? relatedInstance.Name : "未指定")}");
|
||
|
||
// 版本范围验证函数(安全处理 null 边界)
|
||
bool IsVersionSuitable(Version ver)
|
||
{
|
||
return (minVersion is null || ver >= minVersion) && (maxVersion is null || ver <= maxVersion);
|
||
}
|
||
|
||
// ===== 优先级 1:实例专属 Java 偏好 =====
|
||
if (relatedInstance is not null && relatedInstance.PathInstance is not null)
|
||
{
|
||
var rawPreference = Config.Instance.SelectedJava[relatedInstance.PathInstance];
|
||
|
||
if (!string.IsNullOrWhiteSpace(rawPreference))
|
||
{
|
||
var preference = GetInstanceJavaPreference(relatedInstance);
|
||
|
||
// 处理解析成功的偏好
|
||
if (preference is not null)
|
||
switch (true)
|
||
{
|
||
case object _ when preference is ExistingJava: // "exist"
|
||
{
|
||
var existPref = (ExistingJava)preference;
|
||
var candidate = Javas.AddOrGet(existPref.JavaExePath);
|
||
|
||
if (candidate is not null && candidate.IsEnabled)
|
||
{
|
||
if (!IsVersionSuitable(candidate.Installation.Version))
|
||
HintService.Hint(_GetJavaRangeWarning(
|
||
"Minecraft.Launch.Java.Compatibility.InstanceSelectedOutOfRange",
|
||
candidate.Installation.Version,
|
||
minVersion,
|
||
maxVersion));
|
||
ModBase.Log($"[Java] 返回实例 '{relatedInstance.Name}' 指定的 Java: {candidate}");
|
||
return candidate;
|
||
}
|
||
|
||
ModBase.Log($"[Java] 警告:实例指定的 Java 路径无效或不可用: {existPref.JavaExePath}");
|
||
|
||
break;
|
||
}
|
||
|
||
case object _ when preference is UseRelativePath: // "relative"
|
||
{
|
||
var relPref = (UseRelativePath)preference;
|
||
var absPath =
|
||
Path.GetFullPath(Path.Combine(Basics.ExecutableDirectory, relPref.RelativePath));
|
||
|
||
if (Files.IsPathWithinDirectory(absPath, Basics.ExecutableDirectory))
|
||
{
|
||
var candidate = Javas.Get(absPath);
|
||
if (candidate is not null && candidate.IsEnabled)
|
||
{
|
||
if (!IsVersionSuitable(candidate.Installation.Version))
|
||
HintService.Hint(_GetJavaRangeWarning(
|
||
"Minecraft.Launch.Java.Compatibility.RelativePathSelectedOutOfRange",
|
||
candidate.Installation.Version,
|
||
minVersion,
|
||
maxVersion),
|
||
HintType.Error);
|
||
ModBase.Log(
|
||
$"[Java] 返回实例 '{relatedInstance.Name}' 相对路径指定的 Java ({relPref.RelativePath}): {candidate}");
|
||
return candidate;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ModBase.Log($"[Java] 警告:实例相对路径指定的 Java 无效: {absPath}");
|
||
}
|
||
|
||
break;
|
||
}
|
||
|
||
case object _ when preference is UseGlobalPreference: // "global"
|
||
{
|
||
// 不返回,继续到全局设置检查
|
||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 配置为使用全局 Java 设置,继续检查全局配置");
|
||
break;
|
||
}
|
||
|
||
default:
|
||
{
|
||
ModBase.Log($"[Java] 警告:未知的 Java 偏好类型 '{preference}',跳过处理");
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 未指定 Java 偏好(空值),使用自动选择策略");
|
||
}
|
||
else
|
||
{
|
||
ModBase.Log($"[Java] 实例 '{relatedInstance.Name}' 无 Java 偏好配置,使用自动选择策略");
|
||
}
|
||
}
|
||
|
||
// ===== 优先级 2:全局指定的 Java =====
|
||
var globalJavaPath = Config.Launch.SelectedJava;
|
||
if (!string.IsNullOrWhiteSpace(globalJavaPath))
|
||
{
|
||
globalJavaPath = globalJavaPath.Trim();
|
||
var candidate = Javas.AddOrGet(globalJavaPath);
|
||
|
||
if (candidate is not null && candidate.IsEnabled)
|
||
{
|
||
var versionSuitable = IsVersionSuitable(candidate.Installation.Version);
|
||
if (enforceVersionRange && !versionSuitable)
|
||
{
|
||
ModBase.Log($"[Java] 全局指定的 Java 版本不满足强制范围要求,忽略并继续自动搜索: {candidate}");
|
||
}
|
||
else
|
||
{
|
||
if (!versionSuitable)
|
||
HintService.Hint(_GetJavaRangeWarning(
|
||
"Minecraft.Launch.Java.Compatibility.GlobalSelectedOutOfRange",
|
||
candidate.Installation.Version,
|
||
minVersion,
|
||
maxVersion));
|
||
ModBase.Log($"[Java] 返回全局指定的 Java: {candidate}");
|
||
return candidate;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ModBase.Log($"[Java] 警告:全局指定的 Java 路径无效或不可用: {globalJavaPath}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
ModBase.Log("[Java] 无全局 Java 配置,使用自动选择策略");
|
||
}
|
||
|
||
// ===== 优先级 3:自动搜索合适版本 =====
|
||
ModBase.Log("[Java] 开始自动搜索符合版本要求的 Java 运行时");
|
||
Javas.CheckAllAvailability();
|
||
|
||
var reqMin = minVersion ?? new Version(1, 0, 0);
|
||
var reqMax = maxVersion ?? new Version(999, 999, 999);
|
||
|
||
var candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult();
|
||
var ret = candidates.FirstOrDefault();
|
||
|
||
if (ret is null && candidates.Length == 0)
|
||
{
|
||
ModBase.Log("[Java] 未找到符合版本要求的 Java,触发全盘重新扫描");
|
||
Javas.ScanJavaAsync().GetAwaiter().GetResult();
|
||
candidates = Javas.SelectSuitableJavaAsync(reqMin, reqMax).GetAwaiter().GetResult();
|
||
ret = candidates.FirstOrDefault();
|
||
}
|
||
|
||
if (ret is not null)
|
||
ModBase.Log($"[Java] 返回自动选择的 Java: {ret}");
|
||
else
|
||
ModBase.Log("[Java] 最终未能确定可用的 Java 运行时");
|
||
|
||
return ret;
|
||
}
|
||
|
||
private static string _GetJavaRangeWarning(
|
||
string key,
|
||
Version selectedVersion,
|
||
Version? minVersion,
|
||
Version? maxVersion)
|
||
{
|
||
return Lang.Text(
|
||
key,
|
||
selectedVersion,
|
||
minVersion?.ToString() ?? Lang.Text("Minecraft.Launch.Java.Compatibility.NoMinimum"),
|
||
maxVersion?.ToString() ?? Lang.Text("Minecraft.Launch.Java.Compatibility.NoMaximum"));
|
||
}
|
||
|
||
public static JavaPreference GetInstanceJavaPreference(McInstance instance)
|
||
{
|
||
var rawPreference = Config.Instance.SelectedJava[instance.PathInstance];
|
||
|
||
JavaPreference preference = default;
|
||
|
||
// 尝试读取 JSON 配置
|
||
if (!string.IsNullOrEmpty(rawPreference))
|
||
{
|
||
try
|
||
{
|
||
preference = JsonSerializer.Deserialize<JavaPreference>(rawPreference, JsonCompat.SerializerOptions);
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
// ignored
|
||
}
|
||
}
|
||
// 以旧方式读取配置
|
||
if (preference is null)
|
||
{
|
||
var trimmed = rawPreference?.Trim();
|
||
if (string.IsNullOrEmpty(trimmed))
|
||
{
|
||
preference = new AutoSelect();
|
||
}
|
||
else if (trimmed == "使用全局设置")
|
||
{
|
||
preference = new UseGlobalPreference();
|
||
}
|
||
else
|
||
{
|
||
preference = new ExistingJava(trimmed);
|
||
}
|
||
}
|
||
|
||
switch (true)
|
||
{
|
||
case object _ when preference is ExistingJava:
|
||
{
|
||
var m = (ExistingJava)preference;
|
||
if (!Path.IsPathRooted(m.JavaExePath)) preference = new UseGlobalPreference();
|
||
|
||
break;
|
||
}
|
||
case object _ when preference is UseRelativePath:
|
||
{
|
||
var m = (UseRelativePath)preference;
|
||
if (!Files.IsPathWithinDirectory(m.RelativePath, Basics.ExecutableDirectory))
|
||
preference = new UseGlobalPreference();
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
return preference;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否强制指定了 64 位 Java。如果没有强制指定,返回是否安装了 64 位 Java。
|
||
/// </summary>
|
||
public static bool IsGameSet64BitJava(McInstance relatedVersion = null)
|
||
{
|
||
try
|
||
{
|
||
// 检查强制指定
|
||
var userSetup = Config.Launch.SelectedJava;
|
||
if (userSetup.StartsWith("{")) // 旧版本 Json 格式
|
||
{
|
||
var js = ModBase.GetJson(userSetup);
|
||
userSetup = $"{js["Path"]}java.exe";
|
||
Config.Launch.SelectedJava = userSetup;
|
||
}
|
||
|
||
if (relatedVersion is not null)
|
||
{
|
||
var instancePreference = GetInstanceJavaPreference(relatedVersion);
|
||
switch (true)
|
||
{
|
||
case object _ when instancePreference is AutoSelect:
|
||
{
|
||
return Javas.Existing64BitJava();
|
||
}
|
||
case object _ when instancePreference is ExistingJava:
|
||
{
|
||
var m = (ExistingJava)instancePreference;
|
||
var java = Javas.AddOrGet(m.JavaExePath);
|
||
return java is not null && java.Installation.Is64Bit;
|
||
}
|
||
case object _ when instancePreference is UseRelativePath:
|
||
{
|
||
var m = (UseRelativePath)instancePreference;
|
||
var javaExePath = Path.GetFullPath(m.RelativePath);
|
||
if (Files.IsPathWithinDirectory(javaExePath, Basics.ExecutableDirectory))
|
||
{
|
||
var java = Javas.Get(javaExePath);
|
||
return java is not null && java.Installation.Is64Bit;
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!string.IsNullOrEmpty(userSetup) && !File.Exists(userSetup))
|
||
{
|
||
Config.Launch.SelectedJava = "";
|
||
userSetup = string.Empty;
|
||
}
|
||
|
||
if (string.IsNullOrEmpty(userSetup)) return Javas.Existing64BitJava();
|
||
var j = Javas.AddOrGet(userSetup);
|
||
return j is not null && j.Installation.Is64Bit;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ModBase.Log(
|
||
ex,
|
||
"检查 Java 类别时出错",
|
||
ModBase.LogLevel.Feedback,
|
||
userSummary: Lang.Text("Minecraft.Launch.Java.Compatibility.CheckFailed"));
|
||
if (relatedVersion is not null)
|
||
Config.Instance.SelectedJava[relatedVersion.PathInstance] = "使用全局设置";
|
||
Config.Launch.SelectedJava = "";
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
#region 下载
|
||
|
||
/// <summary>
|
||
/// 提示 Java 缺失,并弹窗确认是否自动下载。返回玩家选择是否下载。
|
||
/// </summary>
|
||
public static bool JavaDownloadConfirm(string versionDescription, bool forcedManualDownload = false)
|
||
{
|
||
if (!forcedManualDownload)
|
||
return ModMain.MyMsgBox(
|
||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Message", versionDescription),
|
||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Title"),
|
||
Lang.Text("Minecraft.Launch.Java.AutoDownload.Action"),
|
||
Lang.Text("Common.Action.Cancel")) == 1;
|
||
|
||
ModMain.MyMsgBox(
|
||
Lang.Text("Minecraft.Launch.Java.NotFound.Manual.Message", versionDescription),
|
||
Lang.Text("Minecraft.Launch.Java.NotFound.Title"));
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取下载 Java 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
|
||
/// </summary>
|
||
public static ModLoader.LoaderCombo<string> GetJavaDownloadLoader()
|
||
{
|
||
var javaDownloadLoader = new LoaderDownload(
|
||
Lang.Text("Minecraft.Launch.Java.Task.DownloadFiles"),
|
||
[])
|
||
{
|
||
ProgressWeight = 10d
|
||
};
|
||
|
||
var loader = new ModLoader.LoaderCombo<string>(
|
||
Lang.Text("Minecraft.Launch.Java.Task.Download"),
|
||
[
|
||
new ModLoader.LoaderTask<string, List<DownloadFile>>(
|
||
Lang.Text("Minecraft.Launch.Java.Task.GetDownloadInfo"),
|
||
JavaFileList)
|
||
{
|
||
ProgressWeight = 2d
|
||
},
|
||
javaDownloadLoader
|
||
]);
|
||
|
||
javaDownloadLoader.OnStateChangedThread += (_, newState, _) =>
|
||
{
|
||
switch (newState)
|
||
{
|
||
case ModBase.LoadState.Failed or ModBase.LoadState.Aborted
|
||
when lastJavaBaseDir is not null:
|
||
ModBase.Log(
|
||
$"[Java] 由于下载未完成,清理未下载完成的 Java 文件:{lastJavaBaseDir}",
|
||
ModBase.LogLevel.Debug);
|
||
|
||
ModBase.DeleteDirectory(lastJavaBaseDir);
|
||
break;
|
||
case ModBase.LoadState.Finished:
|
||
Javas.ScanJavaAsync().GetAwaiter().GetResult();
|
||
lastJavaBaseDir = null;
|
||
break;
|
||
}
|
||
};
|
||
|
||
javaDownloadLoader.hasOnStateChangedThread = true;
|
||
return loader;
|
||
}
|
||
|
||
private static string lastJavaBaseDir; // 用于在下载中断或失败时删除未完成下载的 Java 文件夹,防止残留只下了一半但 -version 能跑的 Java
|
||
|
||
private static readonly HashSet<string> ignoreHash = new[]
|
||
{
|
||
"12976a6c2b227cbac58969c1455444596c894656", "c80e4bab46e34d02826eab226a4441d0970f2aba",
|
||
"84d2102ad171863db04e7ee22a259d1f6c5de4a5"
|
||
}.ToHashSet();
|
||
|
||
private static void JavaFileList(ModLoader.LoaderTask<string, List<DownloadFile>> loader)
|
||
{
|
||
ModBase.Log("[Java] 开始获取 Java 下载信息");
|
||
var indexFileStr = ModNet.NetGetCodeByLoader(
|
||
ModDownload.DlVersionListOrder(
|
||
new[]
|
||
{
|
||
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"
|
||
},
|
||
new[]
|
||
{
|
||
"https://bmclapi2.bangbang93.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"
|
||
}), isJson: true);
|
||
// 查找要下载的目标 Java
|
||
string? targetName = null;
|
||
JsonNode? targetValue = null;
|
||
var components =
|
||
(JsonObject)((JsonObject)ModBase.GetJson(indexFileStr))[$"windows-x{(SystemInfo.Is32BitSystem ? "86" : "64")}"];
|
||
if (components.ContainsKey(loader.input)) // 精确匹配
|
||
{
|
||
targetName = loader.input;
|
||
targetValue = components[loader.input];
|
||
}
|
||
else // 模糊匹配
|
||
{
|
||
var match = components.FirstOrDefault(c =>
|
||
c.Value?.AsArray().FirstOrDefault()?["version"]?["name"]?.ToString().StartsWithF(loader.input) ?? false);
|
||
targetName = match.Key;
|
||
targetValue = match.Value;
|
||
if (targetName is null)
|
||
throw new Exception($"未能找到所需的 Java {loader.input}");
|
||
}
|
||
|
||
var targetComponent = targetValue?.AsArray().FirstOrDefault();
|
||
if (targetComponent is null)
|
||
throw new Exception($"Mojang 未提供所需的 Java {loader.input}");
|
||
// 获取文件列表
|
||
var address = (string)targetComponent["manifest"]["url"];
|
||
ModLaunch.McLaunchLog($"准备下载 Java {targetComponent["version"]["name"]}({targetName}):{address}");
|
||
var listFileStr = (JsonObject)Requester.FetchJson(
|
||
ModDownload.DlSourceOrder(new[] { address },
|
||
new[] { address.Replace("piston-meta.mojang.com", "bmclapi2.bangbang93.com") }).First(), RequestParam.WithRetry);
|
||
lastJavaBaseDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||
".minecraft", "runtime", targetName);
|
||
var results = new List<DownloadFile>(listFileStr["files"].AsObject().Count);
|
||
foreach (var File in listFileStr["files"].AsObject())
|
||
{
|
||
if (File.Value?.AsObject()?["downloads"]?["raw"] is null)
|
||
continue;
|
||
|
||
var info = File.Value["downloads"]["raw"].AsObject();
|
||
var checkHash = info["sha1"];
|
||
if (ignoreHash.Contains((string)checkHash))
|
||
continue; // 跳过 3 个无意义大量重复文件(#3827)
|
||
|
||
var checker = new ModBase.FileChecker(actualSize: (long)info["size"], hash: (string)info["sha1"]);
|
||
var filePath = Path.GetFullPath(Path.Combine(lastJavaBaseDir, File.Key));
|
||
if (!Files.IsPathWithinDirectory(filePath, lastJavaBaseDir))
|
||
throw new Exception($"{filePath} 不在 {lastJavaBaseDir} 中");
|
||
|
||
if (checker.Check(filePath) is null)
|
||
continue; // 跳过已存在的文件
|
||
var url = (string)info["url"];
|
||
results.Add(new DownloadFile(
|
||
ModDownload.DlSourceOrder(new[] { url },
|
||
new[] { url.Replace("piston-data.mojang.com", "bmclapi2.bangbang93.com") }), filePath, checker));
|
||
}
|
||
|
||
loader.output = results;
|
||
ModBase.Log($"[Java] 需要下载 {results.Count} 个文件,目标文件夹:{lastJavaBaseDir}");
|
||
}
|
||
|
||
#endregion
|
||
}
|