初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
namespace PCL.Core.Minecraft.Java;
|
||||
|
||||
public enum JavaBrandType
|
||||
{
|
||||
EclipseTemurin,
|
||||
Liberica,
|
||||
Zulu,
|
||||
Corretto,
|
||||
Microsoft,
|
||||
IBMSemeru,
|
||||
Oracle,
|
||||
Dragonwell,
|
||||
TencentKona,
|
||||
OpenJDK,
|
||||
GraalVmCommunity,
|
||||
JetBrains,
|
||||
Unknown
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace PCL.Core.Minecraft.Java;
|
||||
public class JavaConsts
|
||||
{
|
||||
public static readonly string[] ExcludeFolderNames = ["javapath", "java8path", "common files", "netease"];
|
||||
|
||||
public static readonly string[] MostPossibleKeywords =
|
||||
[
|
||||
"java", "jdk", "jre",
|
||||
"dragonwell", "azul", "zulu", "oracle", "open", "amazon", "corretto",
|
||||
"eclipse", "temurin", "hotspot", "semeru", "kona", "bellsoft"
|
||||
];
|
||||
|
||||
public static readonly string[] PossibleKeywords =
|
||||
[
|
||||
"environment", "env", "runtime", "x86_64", "amd64", "arm64", "x64",
|
||||
"pcl", "hmcl", "baka", "minecraft"
|
||||
];
|
||||
|
||||
public static readonly string[] AllKeywords = [.. PossibleKeywords, .. MostPossibleKeywords];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using PCL.Core.Minecraft.Java;
|
||||
|
||||
namespace PCL.Core.Minecraft;
|
||||
|
||||
public sealed class JavaEntry
|
||||
{
|
||||
public required JavaInstallation Installation { get; init; }
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
public JavaSource Source { get; set; } = JavaSource.AutoScanned;
|
||||
|
||||
public override string ToString() =>
|
||||
$"{(IsEnabled ? "[✓]" : "[ ]")} {Installation}";
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using PCL.Core.Utils;
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java;
|
||||
|
||||
public sealed record JavaInstallation(
|
||||
string JavaFolder,
|
||||
Version Version,
|
||||
JavaBrandType Brand,
|
||||
MachineType Architecture,
|
||||
bool Is64Bit,
|
||||
bool IsJre)
|
||||
{
|
||||
public string JavaExePath => Path.Combine(JavaFolder, "java.exe");
|
||||
public string? JavawExePath
|
||||
{
|
||||
get
|
||||
{
|
||||
var javaw = Path.Combine(JavaFolder, "javaw.exe");
|
||||
return File.Exists(javaw) ? javaw : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Java 主版本号(处理 1.8 → 8 的映射)
|
||||
/// </summary>
|
||||
public int MajorVersion => Version.Major == 1 ? Version.Minor : Version.Major;
|
||||
|
||||
/// <summary>
|
||||
/// 检查物理文件是否存在(合理查询,非状态存储)
|
||||
/// </summary>
|
||||
public bool IsStillAvailable => File.Exists(JavaExePath);
|
||||
|
||||
public override string ToString() =>
|
||||
$"{(IsJre ? "JRE" : "JDK")} {MajorVersion} {Brand} {(Is64Bit ? "64 Bit" : "32 Bit")} | {JavaFolder}";
|
||||
|
||||
public string ToDetailedString() =>
|
||||
$"{(IsJre ? "JRE" : "JDK")} {Version} {Brand} {(Is64Bit ? "64 Bit" : "32 Bit")} | {JavaFolder}";
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Minecraft.Java;
|
||||
using PCL.Core.Minecraft.Java.Parser;
|
||||
using PCL.Core.Minecraft.Java.Scanner;
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Utils.Exts;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Text.Json;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.Minecraft;
|
||||
|
||||
public class JavaManager
|
||||
{
|
||||
private const string ModuleName = "JavaManager";
|
||||
private readonly Dictionary<string, JavaEntry> _javaEntrys = new();
|
||||
|
||||
private readonly IJavaParser _parser;
|
||||
private readonly IJavaScanner[] _scanners;
|
||||
|
||||
private readonly SemaphoreSlim _scanLock = new(1, 1);
|
||||
private DateTime _lastScanTime = DateTime.MinValue;
|
||||
private static readonly TimeSpan _MinScanInterval = TimeSpan.FromSeconds(13);
|
||||
|
||||
public JavaManager(
|
||||
IJavaParser parser,
|
||||
params IJavaScanner[] scanners)
|
||||
{
|
||||
_parser = parser;
|
||||
_scanners = scanners;
|
||||
}
|
||||
|
||||
public void SaveConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = _javaEntrys
|
||||
.Select(x => new JavaStorageItem()
|
||||
{
|
||||
Path = x.Value.Installation.JavaExePath,
|
||||
IsEnable = x.Value.IsEnabled,
|
||||
Source = x.Value.Source
|
||||
})
|
||||
.ToArray();
|
||||
States.Game.JavaList = JsonSerializer.Serialize(items, JsonCompat.SerializerOptions);
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, ModuleName, "保存 Java 配置项失败");
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadConfig()
|
||||
{
|
||||
try
|
||||
{
|
||||
var items = JsonSerializer.Deserialize<JavaStorageItem[]>(States.Game.JavaList, JsonCompat.SerializerOptions);
|
||||
if (items is null) return;
|
||||
|
||||
var itemsAdded = new List<JavaEntry>();
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var parserResult = _parser.Parse(item.Path);
|
||||
if (parserResult is null)
|
||||
{
|
||||
LogWrapper.Trace(ModuleName, $"Can not find Java {item.Path}, skip");
|
||||
continue;
|
||||
}
|
||||
|
||||
itemsAdded.Add(new JavaEntry() {
|
||||
Installation = parserResult,
|
||||
IsEnabled = item.IsEnable,
|
||||
Source = item.Source ?? JavaSource.AutoScanned
|
||||
});
|
||||
}
|
||||
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
foreach(var item in itemsAdded)
|
||||
{
|
||||
if (_javaEntrys.TryGetValue(item.Installation.JavaExePath, out var existingRecord))
|
||||
{
|
||||
existingRecord.IsEnabled = item.IsEnabled;
|
||||
existingRecord.Source = item.Source;
|
||||
}
|
||||
else
|
||||
{
|
||||
_javaEntrys.Add(item.Installation.JavaExePath, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, ModuleName, "无法读取 Java 配置项");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 扫描 Java 安装
|
||||
/// </summary>
|
||||
public async Task ScanJavaAsync(bool force = false)
|
||||
{
|
||||
if (ShouldSkip()) return;
|
||||
|
||||
if (!await _scanLock.WaitAsync(TimeSpan.FromSeconds(7))) return;
|
||||
try
|
||||
{
|
||||
if (ShouldSkip()) return;
|
||||
|
||||
await Task.Run(_ScanInternal);
|
||||
_lastScanTime = DateTime.Now;
|
||||
SaveConfig();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_scanLock.Release();
|
||||
}
|
||||
|
||||
bool ShouldSkip()
|
||||
{
|
||||
return !force && (DateTime.Now - _lastScanTime) < _MinScanInterval;
|
||||
}
|
||||
}
|
||||
|
||||
private void _ScanInternal()
|
||||
{
|
||||
var pathSet = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
Parallel.ForEach(_scanners, scanner =>
|
||||
{
|
||||
var temp = new List<string>();
|
||||
scanner.Scan(temp);
|
||||
foreach (var path in temp)
|
||||
{
|
||||
var normalized = _NormalizePath(path);
|
||||
if (!_ShouldExcludePath(normalized))
|
||||
pathSet.TryAdd(normalized, true);
|
||||
}
|
||||
});
|
||||
|
||||
var scannedEntries = pathSet.Keys
|
||||
.Select(_parser.Parse)
|
||||
.Where(inst => inst is not null)
|
||||
.Select(inst => new JavaEntry
|
||||
{
|
||||
Installation = inst!,
|
||||
IsEnabled = _javaEntrys.TryGetValue(_NormalizePath(inst!.JavaExePath), out var existingJava)
|
||||
? existingJava.IsEnabled
|
||||
: _ShouldEnableByDefault(inst!),
|
||||
Source = JavaSource.AutoScanned
|
||||
})
|
||||
.ToList();
|
||||
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
foreach(var entry in scannedEntries)
|
||||
{
|
||||
_javaEntrys[entry.Installation.JavaExePath] = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _ShouldEnableByDefault(JavaInstallation inst)
|
||||
{
|
||||
var libDir = Path.Combine(Directory.GetParent(inst.JavaFolder)!.FullName, "lib");
|
||||
var isUsable = (!inst.IsJre && File.Exists(Path.Combine(libDir, "jvm.lib"))) ||
|
||||
(inst.IsJre && File.Exists(Path.Combine(libDir, "rt.jar")));
|
||||
|
||||
return !((inst.IsJre && inst.MajorVersion > 8) ||
|
||||
(inst.Is64Bit ^ Environment.Is64BitOperatingSystem) ||
|
||||
!isUsable);
|
||||
}
|
||||
|
||||
public List<JavaEntry> GetSortedJavaList()
|
||||
{
|
||||
var ret = _javaEntrys.Values.ToList();
|
||||
ret.Sort((a, b) =>
|
||||
{
|
||||
var versionCmp = a.Installation.Version.CompareTo(b.Installation.Version);
|
||||
if (versionCmp != 0) return versionCmp;
|
||||
return a.Installation.Brand - b.Installation.Brand;
|
||||
});
|
||||
ret.Reverse();
|
||||
return ret;
|
||||
}
|
||||
|
||||
public bool Existing64BitJava()
|
||||
{
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
return _javaEntrys.Any(x => x.Value.Installation.Is64Bit);
|
||||
}
|
||||
}
|
||||
|
||||
public bool ExistAnyJava()
|
||||
{
|
||||
return _javaEntrys.Count != 0;
|
||||
}
|
||||
|
||||
public bool Exist(string javaExePath)
|
||||
{
|
||||
return _javaEntrys.ContainsKey(javaExePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取,如果没有就加入记录
|
||||
/// </summary>
|
||||
/// <param name="javaExePath"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public JavaEntry? AddOrGet(string javaExePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (javaExePath.IsNullOrWhiteSpace() || !File.Exists(javaExePath)) return null;
|
||||
|
||||
var installation = _parser.Parse(javaExePath);
|
||||
if (installation is null) return null;
|
||||
|
||||
var exePath = _NormalizePath(installation.JavaExePath);
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
if (_javaEntrys.TryGetValue(exePath, out var ret))
|
||||
return ret;
|
||||
|
||||
var entry = new JavaEntry
|
||||
{
|
||||
Installation = installation,
|
||||
IsEnabled = _ShouldEnableByDefault(installation),
|
||||
Source = JavaSource.ManualAdded
|
||||
};
|
||||
|
||||
_javaEntrys.Add(exePath, entry);
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, ModuleName, $"Failed to add or get {javaExePath}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// 仅获取,如果没有不增加记录
|
||||
/// </summary>
|
||||
public JavaEntry? Get(string javaExePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (javaExePath.IsNullOrWhiteSpace() || !File.Exists(javaExePath)) return null;
|
||||
|
||||
var installation = _parser.Parse(javaExePath);
|
||||
if (installation is null) return null;
|
||||
|
||||
var exePath = _NormalizePath(installation.JavaExePath);
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
if (_javaEntrys.TryGetValue(exePath, out var ret))
|
||||
return ret;
|
||||
|
||||
var entry = new JavaEntry
|
||||
{
|
||||
Installation = installation,
|
||||
IsEnabled = _ShouldEnableByDefault(installation),
|
||||
Source = JavaSource.ManualAdded
|
||||
};
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, ModuleName, $"Failed to get {javaExePath}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<JavaEntry[]> SelectSuitableJavaAsync(Version minVersion, Version maxVersion)
|
||||
{
|
||||
if (_javaEntrys.Count == 0)
|
||||
await ScanJavaAsync();
|
||||
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
return _javaEntrys
|
||||
.Values.ToList()
|
||||
.Where(j => j.Installation.IsStillAvailable && j.IsEnabled &&
|
||||
IsVersionSuitable(j.Installation.Version, minVersion, maxVersion))
|
||||
.OrderBy(static j => j.Installation.MajorVersion) // 确保首要选择的大版本正确
|
||||
.ThenBy(static j => j.Installation.IsJre) // JDK 优先
|
||||
.ThenBy(static j => j.Installation.Brand) // Java 发行版优选
|
||||
.ThenByDescending(static j => j.Installation.Version) // 优选后小版本号较高的版本
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void CheckAllAvailability()
|
||||
{
|
||||
lock (_javaEntrys)
|
||||
{
|
||||
var keys4Remove = _javaEntrys
|
||||
.Where(kv => !kv.Value.Installation.IsStillAvailable)
|
||||
.Select(kv => kv.Key)
|
||||
.ToArray();
|
||||
foreach (var key in keys4Remove)
|
||||
_javaEntrys.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 路径工具 =====
|
||||
private static string _NormalizePath(string path) =>
|
||||
Path.GetFullPath(path.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
|
||||
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
|
||||
private static bool _ShouldExcludePath(string path) =>
|
||||
path.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
|
||||
.Any(part => JavaConsts.ExcludeFolderNames.Contains(part, StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 将 Java 版本规范化为统一比较格式(1.8.0 → 8.0.0)
|
||||
/// </summary>
|
||||
public static Version NormalizeVersion(Version version) =>
|
||||
version.Major == 1 && version.Minor >= 0
|
||||
? new Version(version.Minor, Math.Max(version.Build, 0), Math.Max(version.Revision, 0))
|
||||
: version;
|
||||
|
||||
// ===== 版本处理工具 =====
|
||||
|
||||
/// <summary>
|
||||
/// 检查版本是否在指定范围内(闭区间)
|
||||
/// </summary>
|
||||
public static bool IsVersionSuitable(Version javaVersion, Version minVersion, Version maxVersion)
|
||||
{
|
||||
var normalizedJava = NormalizeVersion(javaVersion);
|
||||
var normalizedMin = NormalizeVersion(minVersion);
|
||||
var normalizedMax = NormalizeVersion(maxVersion);
|
||||
|
||||
return normalizedJava >= normalizedMin && normalizedJava <= normalizedMax;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using PCL.Core.Minecraft.Java.Parser;
|
||||
using PCL.Core.Minecraft.Java.Scanner;
|
||||
using System.Threading.Tasks;
|
||||
using PCL.Core.App.IoC;
|
||||
|
||||
namespace PCL.Core.Minecraft;
|
||||
|
||||
[LifecycleService(LifecycleState.Loaded)]
|
||||
[LifecycleScope("java", "Java 管理")]
|
||||
public sealed partial class JavaService
|
||||
{
|
||||
|
||||
private static JavaManager? _javaManager;
|
||||
public static JavaManager JavaManager => _javaManager!;
|
||||
|
||||
[LifecycleStart]
|
||||
private static async Task _StartAsync()
|
||||
{
|
||||
if (_javaManager is not null) return;
|
||||
|
||||
Context.Info("Initializing Java Manager...");
|
||||
|
||||
_javaManager = new JavaManager(
|
||||
new PeHeaderParser(),
|
||||
[
|
||||
new RegistryJavaScanner(),
|
||||
new DefaultPathsScanner(),
|
||||
new PathEnvironmentScanner(),
|
||||
new MicrosoftStoreJavaScanner(),
|
||||
new WhereCommandScanner()
|
||||
]);
|
||||
_javaManager.ReadConfig();
|
||||
|
||||
Context.Info("Lookup for local Java...");
|
||||
await _javaManager.ScanJavaAsync();
|
||||
|
||||
var logInfo = string.Join("\n\t", _javaManager.GetSortedJavaList());
|
||||
Context.Info($"Finished to scan java: \n\t{logInfo}");
|
||||
}
|
||||
|
||||
[LifecycleStop]
|
||||
private static void _Stop()
|
||||
{
|
||||
if (_javaManager is null) return;
|
||||
|
||||
_javaManager.SaveConfig();
|
||||
_javaManager = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace PCL.Core.Minecraft.Java;
|
||||
public enum JavaSource
|
||||
{
|
||||
AutoScanned,
|
||||
AutoInstalled,
|
||||
ManualAdded,
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace PCL.Core.Minecraft.Java;
|
||||
|
||||
public class JavaStorageItem
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
public bool IsEnable { get; init; }
|
||||
public JavaSource? Source { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
namespace PCL.Core.Minecraft.Java.Parser;
|
||||
public interface IJavaParser
|
||||
{
|
||||
JavaInstallation? Parse(string javaExePath);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Parser;
|
||||
public class PeHeaderParser : IJavaParser
|
||||
{
|
||||
private static readonly Dictionary<string, JavaBrandType> _BrandMap = new()
|
||||
{
|
||||
["Eclipse"] = JavaBrandType.EclipseTemurin,
|
||||
["Temurin"] = JavaBrandType.EclipseTemurin,
|
||||
["Bellsoft"] = JavaBrandType.Liberica,
|
||||
["Microsoft"] = JavaBrandType.Microsoft,
|
||||
["Amazon"] = JavaBrandType.Corretto,
|
||||
["Azul"] = JavaBrandType.Zulu,
|
||||
["IBM"] = JavaBrandType.IBMSemeru,
|
||||
["Oracle"] = JavaBrandType.Oracle,
|
||||
["Tencent"] = JavaBrandType.TencentKona,
|
||||
["OpenJDK"] = JavaBrandType.OpenJDK,
|
||||
["Alibaba"] = JavaBrandType.Dragonwell,
|
||||
["GraalVM"] = JavaBrandType.GraalVmCommunity,
|
||||
["JetBrains"] = JavaBrandType.JetBrains
|
||||
};
|
||||
|
||||
public JavaInstallation? Parse(string javaExePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(javaExePath))
|
||||
return null;
|
||||
|
||||
LogWrapper.Info("Java", $"解析 {javaExePath} 的 Java 程序信息");
|
||||
|
||||
var versionInfo = FileVersionInfo.GetVersionInfo(javaExePath);
|
||||
var fileVersion = Version.Parse(versionInfo.FileVersion ?? "0.0.0.0");
|
||||
var companyName = _NormalizeCompanyName(versionInfo);
|
||||
var brand = _DetermineBrand(companyName);
|
||||
|
||||
var javaFolder = Path.GetDirectoryName(javaExePath)!;
|
||||
var isJre = !File.Exists(Path.Combine(javaFolder, "javac.exe"));
|
||||
|
||||
var peData = PEHeaderReader.ReadPEHeader(javaExePath);
|
||||
var arch = peData.Machine;
|
||||
var is64Bit = PEHeaderReader.IsMachine64Bit(arch);
|
||||
|
||||
// 可用性检查(不影响模型创建,由调用方决定是否启用)
|
||||
var libDir = Path.Combine(Directory.GetParent(javaFolder)!.FullName, "lib");
|
||||
var isUsable = (!isJre && File.Exists(Path.Combine(libDir, "jvm.lib"))) ||
|
||||
(isJre && File.Exists(Path.Combine(libDir, "rt.jar")));
|
||||
|
||||
return new JavaInstallation(
|
||||
javaFolder,
|
||||
fileVersion,
|
||||
brand,
|
||||
arch,
|
||||
is64Bit,
|
||||
isJre
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, $"[Java] 解析 {javaExePath} 时出错");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string _NormalizeCompanyName(FileVersionInfo info)
|
||||
{
|
||||
var name = info.CompanyName ?? info.FileDescription ?? info.ProductName ?? string.Empty;
|
||||
|
||||
// 修复 Oracle/OpenJDK 混淆问题
|
||||
if (name.Contains("Oracle", StringComparison.OrdinalIgnoreCase) || name == "N/A")
|
||||
{
|
||||
if ((info.FileDescription?.Contains("Java(TM)", StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||
(info.ProductName?.Contains("Java(TM)", StringComparison.OrdinalIgnoreCase) ?? false))
|
||||
return "Oracle";
|
||||
return "OpenJDK";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private static JavaBrandType _DetermineBrand(string output)
|
||||
{
|
||||
var match = _BrandMap.Keys
|
||||
.FirstOrDefault(k => output.Contains(k, StringComparison.OrdinalIgnoreCase));
|
||||
return match is not null ? _BrandMap[match] : JavaBrandType.Unknown;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
using PCL.Core.App;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
|
||||
public class DefaultPathsScanner : IJavaScanner
|
||||
{
|
||||
private const int MaxSearchDepth = 6;
|
||||
|
||||
public void Scan(ICollection<string> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
var searchRoots = _GetSearchRoots();
|
||||
LogWrapper.Info($"[Java] 对下列目录进行广度关键词搜索:{Environment.NewLine}{string.Join(Environment.NewLine, searchRoots)}");
|
||||
|
||||
foreach (var root in searchRoots)
|
||||
{
|
||||
_BfsSearch(root, results);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", "默认路径扫描失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> _GetSearchRoots()
|
||||
{
|
||||
var roots = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), ".minecraft", "runtime"),
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
Path.Combine(Basics.ExecutableDirectory, "PCL")
|
||||
};
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
var keyFolders = new[] { "Program Files", "Program Files (x86)" };
|
||||
var drives = DriveInfo.GetDrives()
|
||||
.Where(d => d.DriveType.Equals(DriveType.Fixed) && d.IsReady)
|
||||
.Select(d => d.Name);
|
||||
|
||||
foreach (var drive in drives)
|
||||
{
|
||||
foreach (var folder in keyFolders)
|
||||
{
|
||||
roots.Add(Path.Combine(drive, folder));
|
||||
}
|
||||
|
||||
// 根目录关键词搜索
|
||||
try
|
||||
{
|
||||
var rootDirs = Directory.EnumerateDirectories(drive)
|
||||
.Where(dir => JavaConsts.MostPossibleKeywords.Any(k =>
|
||||
Path.GetFileName(dir).Contains(k, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
foreach (var dir in rootDirs)
|
||||
roots.Add(dir);
|
||||
}
|
||||
catch (UnauthorizedAccessException) { /* 忽略无权限目录 */ }
|
||||
catch (IOException) { /* 忽略IO错误 */ }
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
|
||||
if (!string.IsNullOrEmpty(programFiles) && Directory.Exists(programFiles))
|
||||
roots.Add(programFiles);
|
||||
if (!string.IsNullOrEmpty(programFilesX86) && Directory.Exists(programFilesX86))
|
||||
roots.Add(programFilesX86);
|
||||
}
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
private static void _BfsSearch(string rootPath, ICollection<string> results)
|
||||
{
|
||||
if (!Directory.Exists(rootPath)) return;
|
||||
|
||||
var queue = new Queue<(string Path, int Depth)>();
|
||||
queue.Enqueue((rootPath, 0));
|
||||
|
||||
while (queue.Count > 0)
|
||||
{
|
||||
var (current, depth) = queue.Dequeue();
|
||||
if (depth > MaxSearchDepth || !Directory.Exists(current)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var subDir in Directory.EnumerateDirectories(current))
|
||||
{
|
||||
var javaExe = Path.Combine(subDir, "java.exe");
|
||||
if (File.Exists(javaExe))
|
||||
{
|
||||
results.Add(javaExe);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_ShouldExploreDeeper(subDir))
|
||||
queue.Enqueue((subDir, depth + 1));
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is UnauthorizedAccessException or IOException or DirectoryNotFoundException)
|
||||
{
|
||||
LogWrapper.Debug($"跳过目录 {current}: {ex.Message}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", $"搜索目录 {current} 时出错");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _ShouldExploreDeeper(string path)
|
||||
{
|
||||
var name = Path.GetFileName(path).AsSpan();
|
||||
|
||||
foreach (var ex in JavaConsts.ExcludeFolderNames)
|
||||
if (name.Contains(ex, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
foreach (var kw in JavaConsts.AllKeywords)
|
||||
if (name.Contains(kw, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return _IsVersionLikeDirectory(name);
|
||||
}
|
||||
|
||||
private static bool _IsVersionLikeDirectory(ReadOnlySpan<char> name)
|
||||
{
|
||||
if (name.IsEmpty || name.Length > 20)
|
||||
return false;
|
||||
|
||||
var hasDigit = false;
|
||||
foreach (var c in name)
|
||||
{
|
||||
if (char.IsDigit(c))
|
||||
{
|
||||
hasDigit = true;
|
||||
}
|
||||
else if (c != '.' && c != '_' && c != '-')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return hasDigit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
public interface IJavaScanner
|
||||
{
|
||||
void Scan(ICollection<string> results);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
public class MicrosoftStoreJavaScanner : IJavaScanner
|
||||
{
|
||||
private const string StorePackagePath =
|
||||
@"Packages\Microsoft.4297127D64EC6_8wekyb3d8bbwe\LocalCache\Local\runtime";
|
||||
|
||||
public void Scan(ICollection<string> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
var basePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
StorePackagePath);
|
||||
|
||||
if (!Directory.Exists(basePath)) return;
|
||||
|
||||
// 第一级:java-runtime* 目录
|
||||
foreach (var runtimeDir in Directory.EnumerateDirectories(basePath))
|
||||
{
|
||||
if (!Path.GetFileName(runtimeDir).StartsWith("java-runtime", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
|
||||
// 第二级:架构目录 (windows-x64等)
|
||||
foreach (var archDir in Directory.EnumerateDirectories(runtimeDir))
|
||||
{
|
||||
// 第三级:版本目录
|
||||
foreach (var versionDir in Directory.EnumerateDirectories(archDir))
|
||||
{
|
||||
var javaExe = Path.Combine(versionDir, "bin", "java.exe");
|
||||
if (File.Exists(javaExe))
|
||||
{
|
||||
LogWrapper.Info($"[Java] 检测到 Microsoft Store Java: {javaExe}");
|
||||
results.Add(javaExe);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", "Microsoft Store Java 扫描失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
|
||||
public class PathEnvironmentScanner : IJavaScanner
|
||||
{
|
||||
public void Scan(ICollection<string> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pathVar = Environment.GetEnvironmentVariable("PATH");
|
||||
if (string.IsNullOrEmpty(pathVar)) return;
|
||||
|
||||
foreach (var dir in pathVar.Split(';', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (!Directory.Exists(dir)) continue;
|
||||
|
||||
var javaExe = Path.Combine(dir, "java.exe");
|
||||
if (File.Exists(javaExe)) results.Add(javaExe);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", "PATH环境变量扫描失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
using Microsoft.Win32;
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
|
||||
public class RegistryJavaScanner : IJavaScanner
|
||||
{
|
||||
private static readonly string[] _RegistryPaths =
|
||||
[
|
||||
@"SOFTWARE\JavaSoft\Java Development Kit",
|
||||
@"SOFTWARE\JavaSoft\Java Runtime Environment",
|
||||
@"SOFTWARE\WOW6432Node\JavaSoft\Java Development Kit",
|
||||
@"SOFTWARE\WOW6432Node\JavaSoft\Java Runtime Environment"
|
||||
];
|
||||
|
||||
private static readonly string[] _BrandRegistryPaths =
|
||||
[
|
||||
@"SOFTWARE\Azul Systems\Zulu",
|
||||
@"SOFTWARE\BellSoft\Liberica"
|
||||
];
|
||||
|
||||
public void Scan(ICollection<string> results)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ScanJavaSoftRegistry(results);
|
||||
_ScanBrandRegistry(results);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", "注册表扫描失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static void _ScanJavaSoftRegistry(ICollection<string> results)
|
||||
{
|
||||
foreach (var regPath in _RegistryPaths)
|
||||
{
|
||||
using var regKey = Registry.LocalMachine.OpenSubKey(regPath);
|
||||
if (regKey is null) continue;
|
||||
|
||||
foreach (var subKeyName in regKey.GetSubKeyNames())
|
||||
{
|
||||
using var subKey = regKey.OpenSubKey(subKeyName);
|
||||
var javaHome = subKey?.GetValue("JavaHome") as string;
|
||||
if (string.IsNullOrEmpty(javaHome) ||
|
||||
Path.GetInvalidPathChars().Any(c => javaHome.Contains(c))) continue;
|
||||
|
||||
var javaExePath = Path.Combine(javaHome, "bin", "java.exe");
|
||||
if (File.Exists(javaExePath)) results.Add(javaExePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void _ScanBrandRegistry(ICollection<string> results)
|
||||
{
|
||||
foreach (var keyPath in _BrandRegistryPaths)
|
||||
{
|
||||
using var brandKey = Registry.LocalMachine.OpenSubKey(keyPath);
|
||||
if (brandKey is null) continue;
|
||||
|
||||
foreach (var subKeyName in brandKey.GetSubKeyNames())
|
||||
{
|
||||
using var subKey = brandKey.OpenSubKey(subKeyName);
|
||||
var installPath = subKey?.GetValue("InstallationPath") as string;
|
||||
if (string.IsNullOrEmpty(installPath) ||
|
||||
Path.GetInvalidPathChars().Any(c => installPath.Contains(c))) continue;
|
||||
|
||||
var javaExePath = Path.Combine(installPath, "bin", "java.exe");
|
||||
if (File.Exists(javaExePath)) results.Add(javaExePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
using PCL.Core.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.Scanner;
|
||||
|
||||
public class WhereCommandScanner : IJavaScanner
|
||||
{
|
||||
public void Scan(ICollection<string> results)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows()) return;
|
||||
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = "where",
|
||||
Arguments = "java",
|
||||
UseShellExecute = false,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
using var proc = Process.Start(psi);
|
||||
if (proc is null) return;
|
||||
|
||||
var output = proc.StandardOutput.ReadToEnd();
|
||||
proc.WaitForExit();
|
||||
|
||||
if (proc.ExitCode != 0) return;
|
||||
|
||||
var paths = output.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(p => p.Trim())
|
||||
.Where(p => File.Exists(p));
|
||||
|
||||
foreach (var path in paths)
|
||||
results.Add(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogWrapper.Error(ex, "Java", "where 命令扫描失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PCL.Core.Minecraft.Java.UserPreference;
|
||||
|
||||
public record AutoSelect : JavaPreference;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PCL.Core.Minecraft.Java.UserPreference;
|
||||
|
||||
public record ExistingJava(string JavaExePath) : JavaPreference;
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace PCL.Core.Minecraft.Java.UserPreference;
|
||||
|
||||
[JsonPolymorphic(TypeDiscriminatorPropertyName = "kind")]
|
||||
[JsonDerivedType(typeof(ExistingJava), "exist")]
|
||||
[JsonDerivedType(typeof(UseGlobalPreference), "global")]
|
||||
[JsonDerivedType(typeof(UseRelativePath), "relative")]
|
||||
[JsonDerivedType(typeof(AutoSelect), "auto")]
|
||||
public abstract record JavaPreference;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PCL.Core.Minecraft.Java.UserPreference;
|
||||
|
||||
public record UseGlobalPreference : JavaPreference;
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace PCL.Core.Minecraft.Java.UserPreference;
|
||||
|
||||
public record UseRelativePath(string RelativePath) : JavaPreference;
|
||||
Reference in New Issue
Block a user