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;
///
/// 防止多个需要 Java 的部分同时要求下载 Java(#3797)。
///
public static object javaLock = new();
///
/// 目前所有可用的 Java。
///
public static JavaManager Javas => JavaService.JavaManager;
///
/// 根据要求返回最适合的 Java,若找不到则返回 Nothing。
/// 最小与最大版本在与输入相同时也会通过。
/// 必须在工作线程调用,且必须包括 SyncLock JavaLock。
///
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(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;
}
///
/// 是否强制指定了 64 位 Java。如果没有强制指定,返回是否安装了 64 位 Java。
///
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 下载
///
/// 提示 Java 缺失,并弹窗确认是否自动下载。返回玩家选择是否下载。
///
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;
}
///
/// 获取下载 Java 的加载器。需要开启 IsForceRestart 以正常刷新 Java 列表。
///
public static ModLoader.LoaderCombo GetJavaDownloadLoader()
{
var javaDownloadLoader = new LoaderDownload(
Lang.Text("Minecraft.Launch.Java.Task.DownloadFiles"),
[])
{
ProgressWeight = 10d
};
var loader = new ModLoader.LoaderCombo(
Lang.Text("Minecraft.Launch.Java.Task.Download"),
[
new ModLoader.LoaderTask>(
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 ignoreHash = new[]
{
"12976a6c2b227cbac58969c1455444596c894656", "c80e4bab46e34d02826eab226a4441d0970f2aba",
"84d2102ad171863db04e7ee22a259d1f6c5de4a5"
}.ToHashSet();
private static void JavaFileList(ModLoader.LoaderTask> 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(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
}