Compare commits
38
Commits
v3.13.0.337
...
v3.13.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82460ebbcb | ||
|
|
3e8a9bf1c4 | ||
|
|
a2db925e86 | ||
|
|
fe70cc3daf | ||
|
|
5991112cd8 | ||
|
|
084d91c62a | ||
|
|
4963f7a5f0 | ||
|
|
f42d5219c2 | ||
|
|
101a9b5d5e | ||
|
|
7cd79a4e2a | ||
|
|
30c3acfb23 | ||
|
|
31f04db555 | ||
|
|
7d41f7bdd3 | ||
|
|
8e6ac947ed | ||
|
|
20e321f502 | ||
|
|
00b323e507 | ||
|
|
333b593c8a | ||
|
|
665f6df142 | ||
|
|
9e16130537 | ||
|
|
5c98d6734d | ||
|
|
c3801a3445 | ||
|
|
1d137a3a88 | ||
|
|
2e8b9ae3ca | ||
|
|
fb750e8717 | ||
|
|
f9ae0902d8 | ||
|
|
8d69cbc02f | ||
|
|
90c2372c92 | ||
|
|
84c4943de5 | ||
|
|
057943b6c0 | ||
|
|
e943cb0aec | ||
|
|
70290e205b | ||
|
|
a73b8c7172 | ||
|
|
a5942c8e91 | ||
|
|
dc6a97520b | ||
|
|
8be3f04cc8 | ||
|
|
8d85b1537d | ||
|
|
b16f2a6ce6 | ||
|
|
e19f175e29 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
code_review:
|
||||
pull_request_opened:
|
||||
code_review: false
|
||||
+5
-1
@@ -64,4 +64,8 @@ language-subtag-registry
|
||||
# test
|
||||
/hmcl.json
|
||||
/.hmcl.json
|
||||
/.hmcl/
|
||||
/.hmcl/
|
||||
|
||||
# AI
|
||||
/.gemini/
|
||||
!/.gemini/config.yaml
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<component name="CopyrightManager">
|
||||
<copyright>
|
||||
<option name="notice" value="Hello Minecraft! Launcher Copyright (C) &#36;today.year huangyuhui <huanghongxun2008@126.com> and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>." />
|
||||
<option name="myName" value="HMCL" />
|
||||
</copyright>
|
||||
</component>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<component name="CopyrightManager">
|
||||
<settings default="HMCL">
|
||||
<LanguageOptions name="JAVA">
|
||||
<option name="fileTypeOverride" value="3" />
|
||||
<option name="addBlankAfter" value="false" />
|
||||
</LanguageOptions>
|
||||
</settings>
|
||||
</component>
|
||||
@@ -231,10 +231,11 @@ public class JFXRippler extends StackPane {
|
||||
*
|
||||
* @return the ripple radius size
|
||||
*/
|
||||
protected double computeRippleRadius() {
|
||||
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
|
||||
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
|
||||
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
|
||||
protected double computeRippleRadius(double centerX, double centerY) {
|
||||
double dx = Math.max(centerX, control.getLayoutBounds().getWidth() - centerX);
|
||||
double dy = Math.max(centerY, control.getLayoutBounds().getHeight() - centerY);
|
||||
|
||||
return (Math.sqrt(dx * dx + dy * dy) / 0.9) + 2;
|
||||
}
|
||||
|
||||
protected void setOverLayBounds(Rectangle overlay) {
|
||||
@@ -473,7 +474,7 @@ public class JFXRippler extends StackPane {
|
||||
super(centerX,
|
||||
centerY,
|
||||
getRipplerRadius() == Region.USE_COMPUTED_SIZE ?
|
||||
computeRippleRadius() : getRipplerRadius(), null);
|
||||
computeRippleRadius(centerX, centerY) : getRipplerRadius(), null);
|
||||
setCache(true);
|
||||
setCacheHint(CacheHint.SPEED);
|
||||
setCacheShape(true);
|
||||
@@ -518,7 +519,7 @@ public class JFXRippler extends StackPane {
|
||||
new KeyValue(opacityProperty(),
|
||||
1,
|
||||
RIPPLE_INTERPOLATOR)
|
||||
), new KeyFrame(Duration.millis(900), inKeyValues));
|
||||
), new KeyFrame(Duration.millis(600), inKeyValues));
|
||||
|
||||
setScaleX(0);
|
||||
setScaleY(0);
|
||||
|
||||
@@ -430,6 +430,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setNativesDirType(vs.getNativesDirType())
|
||||
.setNativesDir(vs.getNativesDir())
|
||||
.setProcessPriority(vs.getProcessPriority())
|
||||
.setGraphicsBackend(vs.getGraphicsBackend())
|
||||
.setRenderer(vs.getRenderer())
|
||||
.setEnableDebugLogOutput(vs.isEnableDebugLogOutput())
|
||||
.setUseNativeGLFW(vs.isUseNativeGLFW())
|
||||
|
||||
@@ -173,27 +173,27 @@ public final class LauncherHelper {
|
||||
}
|
||||
}),
|
||||
Task.composeAsync(() -> {
|
||||
Renderer renderer = setting.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
|| !(setting.getRenderer() instanceof Renderer.Driver renderer)
|
||||
|| renderer.mesaDriverName() == null)
|
||||
return null;
|
||||
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.name().toLowerCase(Locale.ROOT);
|
||||
Library lib = NativePatcher.getWindowsMesaLoader(java, renderer, OperatingSystem.SYSTEM_VERSION);
|
||||
if (lib == null)
|
||||
return null;
|
||||
Path file = dependencyManager.getGameRepository().getLibraryFile(version.get(), lib);
|
||||
if (file.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the libraries directory path, unable to attach software renderer loader");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
String agent = FileUtils.getAbsolutePath(file) + "=" + renderer.mesaDriverName();
|
||||
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version.get(), lib, integrityCheck)) {
|
||||
return new LibraryDownloadTask(dependencyManager, file, lib)
|
||||
.thenRunAsync(() -> javaAgents.add(agent));
|
||||
} else {
|
||||
javaAgents.add(agent);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
@@ -232,6 +232,7 @@ public final class LauncherHelper {
|
||||
if (quickPlayOption != null) {
|
||||
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
|
||||
}
|
||||
|
||||
LaunchOptions launchOptions = launchOptionsBuilder.create();
|
||||
|
||||
LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10));
|
||||
|
||||
@@ -51,7 +51,10 @@ import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
@@ -167,7 +170,7 @@ public final class ModpackHelper {
|
||||
|
||||
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static boolean isExternalGameNameConflicts(String name) {
|
||||
@@ -211,16 +214,16 @@ public final class ModpackHelper {
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else if (modpack.getManifest() instanceof McbbsModpackManifest)
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.defaultScheduler(), success, failure)
|
||||
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
else
|
||||
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
|
||||
.whenComplete(Schedulers.javafx(), success, failure)
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
}
|
||||
|
||||
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
|
||||
@@ -228,7 +231,7 @@ public final class ModpackHelper {
|
||||
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
|
||||
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
|
||||
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
|
||||
.withStagesHints("hmcl.modpack", "hmcl.modpack.download");
|
||||
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
|
||||
default:
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
|
||||
@@ -163,6 +163,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (!future.isDone())
|
||||
future.completeExceptionally(new AuthenticationException("OAuth server is closing"));
|
||||
stop();
|
||||
}
|
||||
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowserAuthorizationCode = new EventManager<>();
|
||||
|
||||
@@ -99,7 +99,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
return getJavaExecutable(platform, MOJANG_JAVA_PREFIX + gameJavaVersion.component());
|
||||
}
|
||||
|
||||
private static void getAllJava(List<JavaRuntime> list, Platform platform, Path platformRoot, boolean isManaged) {
|
||||
private static void getAllJava(List<Path> list, Platform platform, Path platformRoot) {
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(platformRoot)) {
|
||||
for (Path file : stream) {
|
||||
try {
|
||||
@@ -117,8 +117,7 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
if (Files.isDirectory(javaDir)) {
|
||||
JavaManifest manifest = JsonUtils.fromJsonFile(file, JavaManifest.class);
|
||||
list.add(JavaRuntime.of(executable, manifest.getInfo(), isManaged));
|
||||
list.add(executable);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
@@ -131,28 +130,23 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<JavaRuntime> getAllJava(Platform platform) {
|
||||
public Collection<Path> getAllJava(Platform platform) {
|
||||
Path platformRoot = getPlatformRoot(platform);
|
||||
if (!Files.isDirectory(platformRoot))
|
||||
return Collections.emptyList();
|
||||
|
||||
ArrayList<JavaRuntime> list = new ArrayList<>();
|
||||
|
||||
getAllJava(list, platform, platformRoot, true);
|
||||
if (platform.getOperatingSystem() == OperatingSystem.MACOS) {
|
||||
platformRoot = root.resolve(platform.getOperatingSystem().getMojangName() + "-" + platform.getArchitecture().getCheckedName());
|
||||
if (Files.isDirectory(platformRoot))
|
||||
getAllJava(list, platform, platformRoot, false);
|
||||
}
|
||||
ArrayList<Path> list = new ArrayList<>();
|
||||
|
||||
getAllJava(list, platform, platformRoot);
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion) {
|
||||
Path javaDir = getJavaDir(platform, gameJavaVersion);
|
||||
Path tempDir = getPlatformRoot(platform).resolve(".tmp").resolve(javaDir.getFileName());
|
||||
|
||||
return new MojangJavaDownloadTask(downloadProvider, javaDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
|
||||
return new MojangJavaDownloadTask(downloadProvider, javaDir, tempDir, gameJavaVersion, JavaManager.getMojangJavaPlatform(platform)).thenApplyAsync(result -> {
|
||||
Path executable;
|
||||
try {
|
||||
executable = JavaManager.getExecutable(javaDir).toRealPath();
|
||||
@@ -165,16 +159,16 @@ public final class HMCLJavaRepository implements JavaRepository {
|
||||
|
||||
JavaInfo info;
|
||||
if (JavaManager.isCompatible(platform))
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
info = JavaInfoUtils.fromExecutable(executable);
|
||||
else
|
||||
info = new JavaInfo(platform, result.download.getVersion().getName(), null);
|
||||
info = new JavaInfo(platform, result.download().version().name(), null);
|
||||
|
||||
Map<String, Object> update = new LinkedHashMap<>();
|
||||
update.put("provider", "mojang");
|
||||
update.put("component", gameJavaVersion.component());
|
||||
|
||||
Map<String, JavaLocalFiles.Local> files = new LinkedHashMap<>();
|
||||
result.remoteFiles.getFiles().forEach((path, file) -> {
|
||||
result.remoteFiles().getFiles().forEach((path, file) -> {
|
||||
if (file instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
DownloadInfo downloadInfo = ((MojangJavaRemoteFiles.RemoteFile) file).getDownloads().get("raw");
|
||||
if (downloadInfo != null) {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
@@ -27,7 +28,6 @@ import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
@@ -39,35 +39,10 @@ public final class JavaInfoUtils {
|
||||
private JavaInfoUtils() {
|
||||
}
|
||||
|
||||
private static Path tryFindReleaseFile(Path executable) {
|
||||
Path parent = executable.getParent();
|
||||
if (parent != null && parent.getFileName() != null && parent.getFileName().toString().equals("bin")) {
|
||||
Path javaHome = parent.getParent();
|
||||
if (javaHome != null && javaHome.getFileName() != null) {
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
String javaHomeName = javaHome.getFileName().toString();
|
||||
if ((javaHomeName.contains("jre") || javaHomeName.contains("jdk") || javaHomeName.contains("openj9"))
|
||||
&& Files.isRegularFile(releaseFile)) {
|
||||
return releaseFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static @NotNull JavaInfo fromExecutable(Path executable, boolean tryFindReleaseFile) throws IOException {
|
||||
public static @NotNull JavaInfo fromExecutable(Path executable) throws IOException {
|
||||
assert executable.isAbsolute();
|
||||
|
||||
Path releaseFile;
|
||||
if (tryFindReleaseFile && (releaseFile = tryFindReleaseFile(executable)) != null) {
|
||||
try {
|
||||
return JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
Path thisPath = JarUtils.thisJarPath();
|
||||
|
||||
if (thisPath == null) {
|
||||
throw new IOException("Failed to find current HMCL location");
|
||||
}
|
||||
@@ -94,7 +69,6 @@ public final class JavaInfoUtils {
|
||||
? architecture
|
||||
: Architecture.SYSTEM_ARCH);
|
||||
|
||||
|
||||
return new JavaInfo(platform, result.javaVersion, result.javaVendor);
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
@@ -103,14 +77,9 @@ public final class JavaInfoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Result {
|
||||
@SerializedName("os.name")
|
||||
public String osName;
|
||||
@SerializedName("os.arch")
|
||||
public String osArch;
|
||||
@SerializedName("java.version")
|
||||
public String javaVersion;
|
||||
@SerializedName("java.vendor")
|
||||
public String javaVendor;
|
||||
@JsonSerializable
|
||||
private record Result(@SerializedName("os.name") String osName, @SerializedName("os.arch") String osArch,
|
||||
@SerializedName("java.version") String javaVersion,
|
||||
@SerializedName("java.vendor") String javaVendor) {
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.java;
|
||||
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.stream.JsonWriter;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
@@ -30,7 +32,9 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.CacheRepository;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.windows.WinReg;
|
||||
@@ -40,9 +44,14 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
@@ -55,6 +64,16 @@ public final class JavaManager {
|
||||
private JavaManager() {
|
||||
}
|
||||
|
||||
private static final String[] KNOWN_VENDOR_DIRECTORIES = {
|
||||
"Java",
|
||||
"BellSoft",
|
||||
"AdoptOpenJDK",
|
||||
"Zulu",
|
||||
"Microsoft",
|
||||
"Eclipse Foundation",
|
||||
"Semeru"
|
||||
};
|
||||
|
||||
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("java"));
|
||||
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_CURRENT_DIRECTORY.resolve("java"));
|
||||
|
||||
@@ -160,12 +179,12 @@ public final class JavaManager {
|
||||
return javaRuntime;
|
||||
}
|
||||
|
||||
JavaInfo info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
JavaInfo info = JavaInfoUtils.fromExecutable(executable);
|
||||
return JavaRuntime.of(executable, info, false);
|
||||
}
|
||||
|
||||
public static void refresh() {
|
||||
Task.supplyAsync(JavaManager::searchPotentialJavaExecutables).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
Task.supplyAsync(() -> searchPotentialJavaExecutables(false)).whenComplete(Schedulers.javafx(), (result, exception) -> {
|
||||
if (result != null) {
|
||||
LATCH.await();
|
||||
allJava = result;
|
||||
@@ -326,7 +345,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
public static void initialize() {
|
||||
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables();
|
||||
Map<Path, JavaRuntime> allJava = searchPotentialJavaExecutables(true);
|
||||
JavaManager.allJava = allJava;
|
||||
LATCH.countDown();
|
||||
FXUtils.runInFX(() -> updateAllJavaProperty(allJava));
|
||||
@@ -334,56 +353,56 @@ public final class JavaManager {
|
||||
|
||||
// search java
|
||||
|
||||
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables() {
|
||||
Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.SYSTEM_PLATFORM);
|
||||
private static Map<Path, JavaRuntime> searchPotentialJavaExecutables(boolean useCache) {
|
||||
Searcher searcher = new Searcher(Metadata.HMCL_GLOBAL_DIRECTORY.resolve("javaCache.json"));
|
||||
if (useCache)
|
||||
searcher.loadCache();
|
||||
|
||||
searcher.searchAllJavaInRepository(Platform.SYSTEM_PLATFORM);
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86_64);
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.WINDOWS_X86);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86_64);
|
||||
searcher.searchAllJavaInRepository(Platform.WINDOWS_X86);
|
||||
}
|
||||
break;
|
||||
case MACOS:
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
searchAllJavaInRepository(javaRuntimes, Platform.MACOS_X86_64);
|
||||
searcher.searchAllJavaInRepository(Platform.MACOS_X86_64);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
|
||||
queryJavaInRegistryKey(javaRuntimes, WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Runtime Environment");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\Java Development Kit");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JRE");
|
||||
searcher.queryJavaInRegistryKey(WinReg.HKEY.HKEY_LOCAL_MACHINE, "SOFTWARE\\JavaSoft\\JDK");
|
||||
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles", "C:\\Program Files");
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
searchJavaInProgramFiles(javaRuntimes, "ProgramFiles(ARM)", "C:\\Program Files (ARM)");
|
||||
}
|
||||
searcher.searchJavaInProgramFiles("ProgramFiles", "C:\\Program Files");
|
||||
searcher.searchJavaInProgramFiles("ProgramFiles(x86)", "C:\\Program Files (x86)");
|
||||
break;
|
||||
case LINUX:
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/java")); // Oracle RPMs
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib32/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/usr/lib64/jvm")); // General locations
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/java")); // Oracle RPMs
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib32/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of("/usr/lib64/jvm")); // General locations
|
||||
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), "/.sdkman/candidates/java")); // SDKMAN!
|
||||
break;
|
||||
case MACOS:
|
||||
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get("/Library/Java/JavaVirtualMachines"));
|
||||
searchJavaInMacJavaVirtualMachines(javaRuntimes, Paths.get(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
|
||||
searcher.searchJavaInMacJavaVirtualMachines(Path.of("/Library/Java/JavaVirtualMachines"));
|
||||
searcher.searchJavaInMacJavaVirtualMachines(Path.of(System.getProperty("user.home"), "/Library/Java/JavaVirtualMachines"));
|
||||
searcher.tryAddJavaExecutable(Path.of("/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java"));
|
||||
searcher.tryAddJavaExecutable(Path.of("/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/MacOS/itms/java/bin/java"));
|
||||
// Homebrew
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get("/opt/homebrew/opt/java/bin/java"));
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get("/opt/homebrew/Cellar/openjdk"));
|
||||
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Paths.get("/opt/homebrew/Cellar"), "openjdk@*")) {
|
||||
searcher.tryAddJavaExecutable(Path.of("/opt/homebrew/opt/java/bin/java"));
|
||||
searcher.searchAllJavaInDirectory(Path.of("/opt/homebrew/Cellar/openjdk"));
|
||||
try (DirectoryStream<Path> dirs = Files.newDirectoryStream(Path.of("/opt/homebrew/Cellar"), "openjdk@*")) {
|
||||
for (Path dir : dirs) {
|
||||
searchAllJavaInDirectory(javaRuntimes, dir);
|
||||
searcher.searchAllJavaInDirectory(dir);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to get subdirectories of /opt/homebrew/Cellar");
|
||||
@@ -397,16 +416,16 @@ public final class JavaManager {
|
||||
// Search Minecraft bundled runtimes
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && Architecture.SYSTEM_ARCH.isX86()) {
|
||||
FileUtils.tryGetPath(System.getenv("localappdata"), "Packages\\Microsoft.4297127D64EC6_8wekyb3d8bbwe\\LocalCache\\Local\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
|
||||
|
||||
FileUtils.tryGetPath(Lang.requireNonNullElse(System.getenv("ProgramFiles(x86)"), "C:\\Program Files (x86)"), "Minecraft Launcher\\runtime")
|
||||
.ifPresent(it -> searchAllOfficialJava(javaRuntimes, it, false));
|
||||
.ifPresent(it -> searcher.searchAllOfficialJava(it, false));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), ".minecraft/runtime"), false);
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
searchAllOfficialJava(javaRuntimes, Paths.get(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
searcher.searchAllOfficialJava(Path.of(System.getProperty("user.home"), "Library/Application Support/minecraft/runtime"), false);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
searcher.searchAllOfficialJava(CacheRepository.getInstance().getCacheDirectory().resolve("java"), true);
|
||||
|
||||
// Search in PATH.
|
||||
if (System.getenv("PATH") != null) {
|
||||
@@ -420,7 +439,7 @@ public final class JavaManager {
|
||||
}
|
||||
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
searcher.tryAddJavaExecutable(Path.of(path, OperatingSystem.CURRENT_OS.getJavaExecutable()));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
@@ -430,16 +449,17 @@ public final class JavaManager {
|
||||
String[] paths = System.getenv("HMCL_JRES").split(File.pathSeparator);
|
||||
for (String path : paths) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get(path));
|
||||
searcher.tryAddJavaHome(Path.of(path));
|
||||
} catch (InvalidPathException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
searchAllJavaInDirectory(javaRuntimes, Paths.get(System.getProperty("user.home"), ".jdks"));
|
||||
|
||||
searcher.searchAllJavaInDirectory(Path.of(System.getProperty("user.home"), ".jdks"));
|
||||
|
||||
for (String javaPath : ConfigHolder.globalConfig().getUserJava()) {
|
||||
try {
|
||||
tryAddJavaExecutable(javaRuntimes, Paths.get(javaPath));
|
||||
searcher.tryAddJavaExecutable(Path.of(javaPath));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path: " + javaPath);
|
||||
}
|
||||
@@ -447,233 +467,404 @@ public final class JavaManager {
|
||||
|
||||
JavaRuntime currentJava = JavaRuntime.CURRENT_JAVA;
|
||||
if (currentJava != null
|
||||
&& !javaRuntimes.containsKey(currentJava.getBinary())
|
||||
&& !searcher.javaRuntimes.containsKey(currentJava.getBinary())
|
||||
&& !ConfigHolder.globalConfig().getDisabledJava().contains(currentJava.getBinary().toString())) {
|
||||
javaRuntimes.put(currentJava.getBinary(), currentJava);
|
||||
searcher.addResult(currentJava.getBinary(), currentJava);
|
||||
}
|
||||
|
||||
LOG.trace(javaRuntimes.values().stream().sorted()
|
||||
searcher.saveCache();
|
||||
|
||||
LOG.trace(searcher.javaRuntimes.values().stream().sorted()
|
||||
.map(it -> String.format(" - %s %s (%s, %s): %s",
|
||||
it.isJDK() ? "JDK" : "JRE",
|
||||
it.getVersion(),
|
||||
it.getPlatform().getArchitecture().getDisplayName(),
|
||||
Lang.requireNonNullElse(it.getVendor(), "Unknown"),
|
||||
it.getBinary()))
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + javaRuntimes.size() + "\n", "")));
|
||||
|
||||
return javaRuntimes;
|
||||
.collect(Collectors.joining("\n", "Finished Java lookup, found " + searcher.javaRuntimes.size() + "\n", "")));
|
||||
return searcher.javaRuntimes;
|
||||
}
|
||||
|
||||
private static void tryAddJavaHome(Map<Path, JavaRuntime> javaRuntimes, Path javaHome) {
|
||||
Path executable = getExecutable(javaHome);
|
||||
if (!Files.isRegularFile(executable)) {
|
||||
return;
|
||||
private static final class Searcher {
|
||||
private final Path cacheFile;
|
||||
final Map<Path, JavaRuntime> javaRuntimes = new HashMap<>();
|
||||
private final LinkedHashMap<Path, JavaInfoCache> caches = new LinkedHashMap<>();
|
||||
private final Set<Path> failed = new HashSet<>();
|
||||
private boolean needRefreshCache = false;
|
||||
|
||||
Searcher(Path cacheFile) {
|
||||
this.cacheFile = cacheFile;
|
||||
}
|
||||
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to resolve path " + executable, e);
|
||||
return;
|
||||
private static final Pattern CACHE_VERSION_PATTERN = Pattern.compile("(?<major>\\d+)(?:\\.(?<minor>\\d+))?");
|
||||
private static final int CACHE_MAJOR_VERSION = 0;
|
||||
private static final int CACHE_MINOR_VERSION = 0;
|
||||
|
||||
private record JavaInfoCache(String key, JavaInfo info) {
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
void loadCache() {
|
||||
if (Files.notExists(cacheFile))
|
||||
return;
|
||||
|
||||
JavaInfo info = null;
|
||||
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
if (Files.exists(releaseFile)) {
|
||||
try {
|
||||
info = JavaInfo.fromReleaseFile(releaseFile);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read release file " + releaseFile, e);
|
||||
JsonObject jsonFile = JsonUtils.fromJsonFile(cacheFile, JsonObject.class);
|
||||
JsonElement fileVersion = jsonFile.get("version");
|
||||
|
||||
Matcher matcher;
|
||||
if (fileVersion instanceof JsonPrimitive version
|
||||
&& (matcher = CACHE_VERSION_PATTERN.matcher(version.getAsString())).matches()) {
|
||||
int major = Integer.parseInt(matcher.group("major"));
|
||||
|
||||
String minorString = matcher.group("minor");
|
||||
int minor = minorString != null ? Integer.parseInt(minorString) : 0;
|
||||
|
||||
if (major != CACHE_MAJOR_VERSION || minor < CACHE_MINOR_VERSION)
|
||||
throw new IOException("Unsupported cache file, version: %s".formatted(version.getAsString()));
|
||||
} else
|
||||
throw new IOException("Invalid version JSON: " + fileVersion);
|
||||
|
||||
JsonArray cachesArray = jsonFile.getAsJsonArray("caches");
|
||||
|
||||
for (JsonElement element : cachesArray) {
|
||||
try {
|
||||
var obj = (JsonObject) element;
|
||||
|
||||
Path realPath = Path.of(obj.getAsJsonPrimitive("path").getAsString()).toRealPath();
|
||||
String key = obj.getAsJsonPrimitive("key").getAsString();
|
||||
|
||||
OperatingSystem osName = OperatingSystem.parseOSName(obj.getAsJsonPrimitive("os.name").getAsString());
|
||||
Architecture osArch = Architecture.parseArchName(obj.getAsJsonPrimitive("os.arch").getAsString());
|
||||
String javaVersion = obj.getAsJsonPrimitive("java.version").getAsString();
|
||||
|
||||
JavaInfo.Builder infoBuilder = JavaInfo.newBuilder(Platform.getPlatform(osName, osArch), javaVersion);
|
||||
|
||||
if (obj.get("java.vendor") instanceof JsonPrimitive vendor)
|
||||
infoBuilder.setVendor(vendor.getAsString());
|
||||
|
||||
caches.put(realPath, new JavaInfoCache(key, infoBuilder.build()));
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Invalid cache: " + element);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
LOG.warning("Failed to load cache file: " + cacheFile);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (info == null) {
|
||||
void saveCache() {
|
||||
if (!needRefreshCache)
|
||||
return;
|
||||
|
||||
needRefreshCache = false;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, false);
|
||||
FileUtils.saveSafely(cacheFile, output -> {
|
||||
try (var writer = new JsonWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8))) {
|
||||
writer.beginObject();
|
||||
|
||||
writer.name("version").value("%d.%d".formatted(CACHE_MAJOR_VERSION, CACHE_MINOR_VERSION));
|
||||
|
||||
writer.name("caches");
|
||||
writer.beginArray();
|
||||
for (Map.Entry<Path, JavaInfoCache> entry : caches.entrySet()) {
|
||||
Path path = entry.getKey();
|
||||
JavaInfoCache cache = entry.getValue();
|
||||
JavaInfo info = cache.info();
|
||||
|
||||
writer.beginObject();
|
||||
|
||||
writer.name("path").value(path.toString());
|
||||
writer.name("key").value(cache.key());
|
||||
|
||||
writer.name("os.name").value(info.getPlatform().os().getCheckedName());
|
||||
writer.name("os.arch").value(info.getPlatform().arch().getCheckedName());
|
||||
writer.name("java.version").value(info.getVersion());
|
||||
if (info.getVendor() != null)
|
||||
writer.name("java.vendor").value(info.getVendor());
|
||||
|
||||
writer.endObject();
|
||||
}
|
||||
writer.endArray();
|
||||
|
||||
writer.endObject();
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to save cache file: " + cacheFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable String createCacheKey(Path realPath) {
|
||||
Path binDir = realPath.getParent();
|
||||
if (binDir == null || !FileUtils.getName(binDir).equals("bin"))
|
||||
return null;
|
||||
|
||||
if (Files.isRegularFile(realPath.resolveSibling("ikvm.properties")))
|
||||
return null;
|
||||
|
||||
Path javaHome = binDir.getParent();
|
||||
if (javaHome == null)
|
||||
return null;
|
||||
|
||||
Path libDir = javaHome.resolve("lib");
|
||||
if (!Files.isDirectory(libDir))
|
||||
return null;
|
||||
|
||||
BasicFileAttributes launcherAttributes;
|
||||
String releaseHash = null;
|
||||
BasicFileAttributes coreLibsAttributes = null;
|
||||
|
||||
try {
|
||||
launcherAttributes = Files.readAttributes(realPath, BasicFileAttributes.class);
|
||||
|
||||
Path releaseFile = javaHome.resolve("release");
|
||||
if (Files.exists(releaseFile)) {
|
||||
releaseHash = DigestUtils.digestToString("SHA-1", releaseFile);
|
||||
} else {
|
||||
Path coreLibsFile = libDir.resolve("rt.jar");
|
||||
if (!Files.isRegularFile(coreLibsFile)) {
|
||||
coreLibsFile = javaHome.resolve("jre/lib/rt.jar");
|
||||
if (!Files.isRegularFile(coreLibsFile))
|
||||
return null;
|
||||
|
||||
coreLibsAttributes = Files.readAttributes(coreLibsFile, BasicFileAttributes.class);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to create cache key for " + realPath, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
joiner.add("sz:" + launcherAttributes.size());
|
||||
joiner.add("lm:" + launcherAttributes.lastModifiedTime().toMillis());
|
||||
|
||||
if (releaseHash != null)
|
||||
joiner.add(releaseHash);
|
||||
|
||||
if (coreLibsAttributes != null) {
|
||||
joiner.add("rsz:" + coreLibsAttributes.size());
|
||||
joiner.add("rlm:" + coreLibsAttributes.lastModifiedTime().toMillis());
|
||||
}
|
||||
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
void addResult(Path realPath, JavaRuntime javaRuntime) {
|
||||
javaRuntimes.put(realPath, javaRuntime);
|
||||
}
|
||||
|
||||
void tryAddJavaHome(Path javaHome) {
|
||||
tryAddJavaExecutable(getExecutable(javaHome));
|
||||
}
|
||||
|
||||
void tryAddJavaExecutable(Path executable) {
|
||||
tryAddJavaExecutable(executable, false);
|
||||
}
|
||||
|
||||
void tryAddJavaExecutable(Path executable, boolean isManaged) {
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable)
|
||||
|| failed.contains(executable)
|
||||
|| ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String cacheKey = createCacheKey(executable);
|
||||
if (cacheKey != null) {
|
||||
JavaInfoCache cache = caches.get(executable);
|
||||
if (cache != null) {
|
||||
if (isCompatible(cache.info().getPlatform()) && cacheKey.equals(cache.key())) {
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, cache.info(), isManaged));
|
||||
return;
|
||||
} else {
|
||||
caches.remove(executable);
|
||||
needRefreshCache = true;
|
||||
}
|
||||
}
|
||||
} else if (caches.remove(executable) != null) {
|
||||
needRefreshCache = true;
|
||||
}
|
||||
|
||||
JavaInfo info;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
failed.add(executable);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cacheKey != null) {
|
||||
caches.put(executable, new JavaInfoCache(cacheKey, info));
|
||||
needRefreshCache = true;
|
||||
}
|
||||
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, isManaged));
|
||||
}
|
||||
|
||||
void tryAddJavaInComponentDir(String platform, Path component, boolean verify) {
|
||||
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
|
||||
if (!Files.isRegularFile(sha1File))
|
||||
return;
|
||||
|
||||
Path dir = component.resolve(platform).resolve(component.getFileName());
|
||||
|
||||
if (verify) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
int idx = line.indexOf(" /#//");
|
||||
if (idx <= 0)
|
||||
throw new IOException("Illegal line: " + line);
|
||||
|
||||
Path file = dir.resolve(line.substring(0, idx));
|
||||
|
||||
// Should we check the sha1 of files? This will take a lot of time.
|
||||
if (Files.notExists(file))
|
||||
throw new NoSuchFileException(file.toAbsolutePath().toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to verify Java in " + component, e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(macPath);
|
||||
return;
|
||||
} else
|
||||
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
|
||||
}
|
||||
|
||||
tryAddJavaHome(dir);
|
||||
}
|
||||
|
||||
void searchAllJavaInRepository(Platform platform) {
|
||||
for (Path java : REPOSITORY.getAllJava(platform)) {
|
||||
tryAddJavaExecutable(java, true);
|
||||
}
|
||||
|
||||
for (Path java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
tryAddJavaExecutable(java, true);
|
||||
}
|
||||
|
||||
if (platform.os() == OperatingSystem.MACOS) {
|
||||
// In the past, we used 'osx' as the checked name for macOS
|
||||
Path platformRoot = REPOSITORY.getPlatformRoot(platform).resolveSibling("osx-" + platform.getArchitecture().getCheckedName());
|
||||
searchAllJavaInDirectory(platformRoot);
|
||||
}
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform()))
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
void searchAllOfficialJava(Path directory, boolean verify) {
|
||||
if (!Files.isDirectory(directory))
|
||||
return;
|
||||
// Examples:
|
||||
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
|
||||
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
|
||||
|
||||
private static void tryAddJavaExecutable(Map<Path, JavaRuntime> javaRuntimes, Path executable) {
|
||||
try {
|
||||
executable = executable.toRealPath();
|
||||
} catch (IOException e) {
|
||||
return;
|
||||
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
|
||||
if (javaPlatform != null) {
|
||||
searchAllOfficialJava(directory, javaPlatform, verify);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
}
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
|
||||
if (javaRuntimes.containsKey(executable) || ConfigHolder.globalConfig().getDisabledJava().contains(executable.toString())) {
|
||||
return;
|
||||
}
|
||||
|
||||
JavaInfo info = null;
|
||||
try {
|
||||
info = JavaInfoUtils.fromExecutable(executable, true);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to lookup Java executable at " + executable, e);
|
||||
}
|
||||
|
||||
if (info != null && isCompatible(info.getPlatform())) {
|
||||
javaRuntimes.put(executable, JavaRuntime.of(executable, info, false));
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryAddJavaInComponentDir(Map<Path, JavaRuntime> javaRuntimes, String platform, Path component, boolean verify) {
|
||||
Path sha1File = component.resolve(platform).resolve(component.getFileName() + ".sha1");
|
||||
if (!Files.isRegularFile(sha1File))
|
||||
return;
|
||||
|
||||
Path dir = component.resolve(platform).resolve(component.getFileName());
|
||||
|
||||
if (verify) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(sha1File)) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) continue;
|
||||
|
||||
int idx = line.indexOf(" /#//");
|
||||
if (idx <= 0)
|
||||
throw new IOException("Illegal line: " + line);
|
||||
|
||||
Path file = dir.resolve(line.substring(0, idx));
|
||||
|
||||
// Should we check the sha1 of files? This will take a lot of time.
|
||||
if (Files.notExists(file))
|
||||
throw new NoSuchFileException(file.toAbsolutePath().toString());
|
||||
void searchAllOfficialJava(Path directory, String platform, boolean verify) {
|
||||
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
|
||||
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
|
||||
for (Path component : dir) {
|
||||
tryAddJavaInComponentDir(platform, component, verify);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to verify Java in " + component, e);
|
||||
return;
|
||||
LOG.warning("Failed to list java-runtime directory " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
Path macPath = dir.resolve("jre.bundle/Contents/Home");
|
||||
if (Files.exists(macPath)) {
|
||||
tryAddJavaHome(javaRuntimes, macPath);
|
||||
void searchAllJavaInDirectory(Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
} else
|
||||
LOG.warning("The Java is not in 'jre.bundle/Contents/Home'");
|
||||
}
|
||||
}
|
||||
|
||||
tryAddJavaHome(javaRuntimes, dir);
|
||||
}
|
||||
|
||||
private static void searchAllJavaInRepository(Map<Path, JavaRuntime> javaRuntimes, Platform platform) {
|
||||
for (JavaRuntime java : REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
|
||||
for (JavaRuntime java : LOCAL_REPOSITORY.getAllJava(platform)) {
|
||||
javaRuntimes.put(java.getBinary(), java);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, boolean verify) {
|
||||
if (!Files.isDirectory(directory))
|
||||
return;
|
||||
// Examples:
|
||||
// $HOME/Library/Application Support/minecraft/runtime/java-runtime-beta/mac-os/java-runtime-beta/jre.bundle/Contents/Home
|
||||
// $HOME/.minecraft/runtime/java-runtime-beta/linux/java-runtime-beta
|
||||
|
||||
String javaPlatform = getMojangJavaPlatform(Platform.SYSTEM_PLATFORM);
|
||||
if (javaPlatform != null) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, javaPlatform, verify);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86_64), verify);
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(subDir);
|
||||
}
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.WINDOWS_X86), verify);
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && Architecture.CURRENT_ARCH == Architecture.ARM64) {
|
||||
searchAllOfficialJava(javaRuntimes, directory, getMojangJavaPlatform(Platform.MACOS_X86_64), verify);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllOfficialJava(Map<Path, JavaRuntime> javaRuntimes, Path directory, String platform, boolean verify) {
|
||||
try (DirectoryStream<Path> dir = Files.newDirectoryStream(directory)) {
|
||||
// component can be jre-legacy, java-runtime-alpha, java-runtime-beta, java-runtime-gamma or any other being added in the future.
|
||||
for (Path component : dir) {
|
||||
tryAddJavaInComponentDir(javaRuntimes, platform, component, verify);
|
||||
void searchJavaInProgramFiles(String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Path.of(programFiles);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list java-runtime directory " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchAllJavaInDirectory(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(javaRuntimes, subDir);
|
||||
for (String vendor : KNOWN_VENDOR_DIRECTORIES) {
|
||||
searchAllJavaInDirectory(path.resolve(vendor));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchJavaInProgramFiles(Map<Path, JavaRuntime> javaRuntimes, String env, String defaultValue) {
|
||||
String programFiles = Lang.requireNonNullElse(System.getenv(env), defaultValue);
|
||||
Path path;
|
||||
try {
|
||||
path = Paths.get(programFiles);
|
||||
} catch (InvalidPathException ignored) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (String vendor : new String[]{"Java", "BellSoft", "AdoptOpenJDK", "Zulu", "Microsoft", "Eclipse Foundation", "Semeru"}) {
|
||||
searchAllJavaInDirectory(javaRuntimes, path.resolve(vendor));
|
||||
}
|
||||
}
|
||||
|
||||
private static void searchJavaInMacJavaVirtualMachines(Map<Path, JavaRuntime> javaRuntimes, Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(javaRuntimes, subDir.resolve("Contents/Home"));
|
||||
void searchJavaInMacJavaVirtualMachines(Path directory) {
|
||||
if (!Files.isDirectory(directory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(directory)) {
|
||||
for (Path subDir : stream) {
|
||||
tryAddJavaHome(subDir.resolve("Contents/Home"));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to find Java in " + directory, e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==== Windows Registry Support ====
|
||||
private static void queryJavaInRegistryKey(Map<Path, JavaRuntime> javaRuntimes, WinReg.HKEY hkey, String location) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null)
|
||||
return;
|
||||
// ==== Windows Registry Support ====
|
||||
void queryJavaInRegistryKey(WinReg.HKEY hkey, String location) {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null)
|
||||
return;
|
||||
|
||||
for (String java : reg.querySubKeys(hkey, location)) {
|
||||
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
|
||||
continue;
|
||||
Object home = reg.queryValue(hkey, java, "JavaHome");
|
||||
if (home instanceof String) {
|
||||
try {
|
||||
tryAddJavaHome(javaRuntimes, Paths.get((String) home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
for (String java : reg.querySubKeys(hkey, location)) {
|
||||
if (!reg.querySubKeys(hkey, java).contains(java + "\\MSI"))
|
||||
continue;
|
||||
if (reg.queryValue(hkey, java, "JavaHome") instanceof String home) {
|
||||
try {
|
||||
tryAddJavaHome(Path.of(home));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Invalid Java path in system registry: " + home);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -603,6 +603,20 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
processPriorityProperty.set(processPriority);
|
||||
}
|
||||
|
||||
private final ObjectProperty<GraphicsAPI> graphicsBackend = new SimpleObjectProperty<>(this, "graphicsBackend", GraphicsAPI.DEFAULT);
|
||||
|
||||
public ObjectProperty<GraphicsAPI> graphicsBackendProperty() {
|
||||
return graphicsBackend;
|
||||
}
|
||||
|
||||
public GraphicsAPI getGraphicsBackend() {
|
||||
return graphicsBackendProperty().get();
|
||||
}
|
||||
|
||||
public void setGraphicsBackend(GraphicsAPI api) {
|
||||
graphicsBackendProperty().set(api);
|
||||
}
|
||||
|
||||
private final ObjectProperty<Renderer> rendererProperty = new SimpleObjectProperty<>(this, "renderer", Renderer.DEFAULT);
|
||||
|
||||
public Renderer getRenderer() {
|
||||
@@ -820,8 +834,9 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
obj.addProperty("java", java);
|
||||
|
||||
obj.addProperty("graphicsBackend", src.getGraphicsBackend().name());
|
||||
obj.addProperty("renderer", src.getRenderer().name());
|
||||
if (src.getRenderer() == Renderer.LLVMPIPE)
|
||||
if (src.getRenderer() == Renderer.OpenGL.LLVMPIPE)
|
||||
obj.addProperty("useSoftwareRenderer", true);
|
||||
|
||||
return obj;
|
||||
@@ -894,16 +909,19 @@ public final class VersionSetting implements Cloneable, Observable {
|
||||
}
|
||||
|
||||
vs.setRenderer(Optional.ofNullable(obj.get("renderer")).map(JsonElement::getAsString)
|
||||
.map(Renderer::of).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.OpenGL.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
|
||||
vs.setGraphicsBackend(Optional.ofNullable(obj.get("graphicsBackend")).map(JsonElement::getAsString)
|
||||
.flatMap(name -> {
|
||||
try {
|
||||
return Optional.of(Renderer.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
return Optional.of(GraphicsAPI.valueOf(name.toUpperCase(Locale.ROOT)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}).orElseGet(() -> {
|
||||
boolean useSoftwareRenderer = Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false);
|
||||
return useSoftwareRenderer ? Renderer.LLVMPIPE : Renderer.DEFAULT;
|
||||
}));
|
||||
}).orElseGet(() -> vs.getRenderer() instanceof Renderer.Driver renderer ? renderer.api() : GraphicsAPI.DEFAULT));
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -105,9 +105,14 @@ public final class Controllers {
|
||||
GameListPage gameListPage = new GameListPage();
|
||||
gameListPage.selectedProfileProperty().bindBidirectional(Profiles.selectedProfileProperty());
|
||||
gameListPage.profilesProperty().bindContent(Profiles.profilesProperty());
|
||||
FXUtils.applyDragListener(gameListPage, ModpackHelper::isFileModpackByExtension, modpacks -> {
|
||||
Path modpack = modpacks.get(0);
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), modpack), i18n("install.modpack"));
|
||||
FXUtils.applyDragListener(gameListPage, file -> ModpackHelper.isFileModpackByExtension(file) || "json".equalsIgnoreCase(FileUtils.getNameWithoutExtension(file)), files -> {
|
||||
Path file = files.get(0);
|
||||
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Versions.installFromJson(Profiles.getSelectedProfile(), file);
|
||||
}
|
||||
});
|
||||
return gameListPage;
|
||||
});
|
||||
|
||||
@@ -170,6 +170,7 @@ public final class LogWindow extends Stage {
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
|
||||
listView.getStyleClass().add("no-horizontal-scrollbar");
|
||||
listView.getProperties().put("no-smooth-scrolling", true);
|
||||
listView.setItems(FXCollections.observableList(new CircularArrayList<>(logs.size())));
|
||||
|
||||
|
||||
@@ -164,6 +164,9 @@ public class AccountListItem extends RadioButton {
|
||||
if (skinImg.isError()) {
|
||||
throw new InvalidSkinException("Failed to read skin image", skinImg.getException());
|
||||
}
|
||||
if (skinImg.getWidth() != 64 || (skinImg.getHeight() != 32 && skinImg.getHeight() != 64)) {
|
||||
throw new InvalidSkinException("Invalid skin size");
|
||||
}
|
||||
NormalizedSkin skin = new NormalizedSkin(skinImg);
|
||||
String model = skin.isSlim() ? "slim" : "";
|
||||
LOG.info("Uploading skin [" + selectedFile + "], model [" + model + "]");
|
||||
|
||||
@@ -115,7 +115,7 @@ public final class AddAuthlibInjectorServerPane extends TransitionPane implement
|
||||
GridPane.setRowIndex(lblServerName, 1);
|
||||
|
||||
lblServerWarning = new Label(i18n("account.injector.http"));
|
||||
lblServerWarning.setStyle("-fx-text-fill: red;");
|
||||
lblServerWarning.setStyle("-fx-text-fill: -monet-error;");
|
||||
GridPane.setColumnIndex(lblServerWarning, 0);
|
||||
GridPane.setRowIndex(lblServerWarning, 2);
|
||||
lblServerWarning.managedProperty().bind(lblServerWarning.visibleProperty());
|
||||
|
||||
@@ -28,6 +28,7 @@ import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
@@ -49,6 +50,8 @@ public class LineSelectButton<T> extends LineButton {
|
||||
private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
private JFXPopup popup;
|
||||
@SuppressWarnings({"FieldCanBeLocal", "unused"})
|
||||
private ObservableList<Node> popupItems; // keep a reference
|
||||
|
||||
public LineSelectButton() {
|
||||
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
|
||||
@@ -87,7 +90,7 @@ public class LineSelectButton<T> extends LineButton {
|
||||
|
||||
ripplerContainer.addEventFilter(ScrollEvent.ANY, ignored -> popup.hide());
|
||||
|
||||
Bindings.bindContent(popupMenu.getContent(), MappedObservableList.create(itemsProperty(), item -> {
|
||||
Bindings.bindContent(popupMenu.getContent(), popupItems = MappedObservableList.create(itemsProperty(), item -> {
|
||||
VBox vbox = new VBox();
|
||||
|
||||
var itemTitleLabel = new Label();
|
||||
|
||||
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import org.jackhuang.hmcl.theme.Themes;
|
||||
|
||||
public class MenuSeparator extends StackPane {
|
||||
|
||||
@@ -28,7 +28,7 @@ public class MenuSeparator extends StackPane {
|
||||
Rectangle rect = new Rectangle();
|
||||
rect.widthProperty().bind(widthProperty().add(-14));
|
||||
rect.setHeight(1);
|
||||
rect.setFill(Color.GRAY);
|
||||
rect.fillProperty().bind(Themes.colorSchemeProperty().getOutlineVariant());
|
||||
maxHeightProperty().set(10);
|
||||
setPadding(new Insets(3));
|
||||
getChildren().setAll(rect);
|
||||
|
||||
@@ -147,6 +147,10 @@ public class TwoLineListItem extends VBox {
|
||||
subtitleProperty().set(subtitle);
|
||||
}
|
||||
|
||||
public HBox getFirstLine() {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
public Label getTitleLabel() {
|
||||
return lblTitle;
|
||||
}
|
||||
@@ -168,6 +172,7 @@ public class TwoLineListItem extends VBox {
|
||||
Bindings.bindContent(tagsBox.getChildren(), tags);
|
||||
|
||||
var scrollPane = new ScrollPane(tagsBox);
|
||||
scrollPane.setMinSize(0, 0);
|
||||
HBox.setHgrow(scrollPane, Priority.ALWAYS);
|
||||
lblTitle.setMinWidth(Label.USE_PREF_SIZE);
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
|
||||
@@ -274,6 +274,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canBack) {
|
||||
JFXButton backNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(backNavButton);
|
||||
backNavButton.setFocusTraversable(false);
|
||||
backNavButton.setGraphic(SVG.ARROW_BACK.createIcon(Themes.titleFillProperty()));
|
||||
backNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
@@ -285,6 +286,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
|
||||
if (canClose) {
|
||||
JFXButton closeNavButton = new JFXButton();
|
||||
skinnable.forbidDraggingWindow(closeNavButton);
|
||||
closeNavButton.setFocusTraversable(false);
|
||||
closeNavButton.setGraphic(SVG.CLOSE.createIcon(Themes.titleFillProperty()));
|
||||
closeNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
@@ -349,13 +351,9 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
refreshNavButton.setGraphic(SVG.REFRESH.createIcon(Themes.titleFillProperty()));
|
||||
refreshNavButton.getStyleClass().add("jfx-decorator-button");
|
||||
refreshNavButton.onActionProperty().bind(skinnable.onRefreshNavButtonActionProperty());
|
||||
skinnable.forbidDraggingWindow(refreshNavButton);
|
||||
|
||||
Rectangle separator = new Rectangle();
|
||||
separator.visibleProperty().bind(refreshNavButton.visibleProperty());
|
||||
separator.heightProperty().bind(navBar.heightProperty());
|
||||
separator.setFill(Color.GRAY);
|
||||
|
||||
navRight.getChildren().setAll(refreshNavButton, separator);
|
||||
navRight.getChildren().setAll(refreshNavButton);
|
||||
navBar.setRight(navRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,13 +54,18 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
|
||||
@@ -134,6 +139,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
|
||||
|
||||
Set<String> existingFiles;
|
||||
|
||||
try (var list = Files.list(runDirectory.resolve(subdirectoryName))) {
|
||||
existingFiles = list.map(Path::getFileName)
|
||||
.map(Path::toString)
|
||||
.collect(Collectors.toSet());
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to list files in " + runDirectory.resolve(subdirectoryName), e);
|
||||
existingFiles = Set.of();
|
||||
}
|
||||
|
||||
Set<String> finalExistingFiles = existingFiles;
|
||||
|
||||
Controllers.prompt(i18n("archive.file.name"), (result, handler) -> {
|
||||
Path dest = runDirectory.resolve(subdirectoryName).resolve(result);
|
||||
|
||||
@@ -153,7 +171,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
}
|
||||
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
|
||||
handler.resolve();
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid));
|
||||
}, file.getFile().getFilename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -97,8 +97,7 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
|
||||
graphic.setMouseTransparent(true);
|
||||
graphic.setLeft(new TwoLineListItem(i18n("modpack.choose." + type), i18n("modpack.choose." + type + ".detail")));
|
||||
|
||||
SVGPath arrow = new SVGPath();
|
||||
arrow.setContent(SVG.ARROW_FORWARD.getPath());
|
||||
SVGPath arrow = SVG.ARROW_FORWARD.createIcon();
|
||||
BorderPane.setAlignment(arrow, Pos.CENTER);
|
||||
graphic.setRight(arrow);
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Result;
|
||||
@@ -233,7 +234,6 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
downloadButton.setOnAction(e -> onDownload());
|
||||
downloadButton.getStyleClass().add("dialog-accept");
|
||||
downloadButton.disableProperty().bind(Bindings.isNull(remoteVersionBox.getSelectionModel().selectedItemProperty()));
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
|
||||
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
|
||||
cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent()));
|
||||
@@ -300,6 +300,11 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
remoteVersionBox.getSelectionModel().selectFirst();
|
||||
});
|
||||
|
||||
SpinnerPane spinnerPane = new SpinnerPane();
|
||||
spinnerPane.getStyleClass().add("small-spinner-pane");
|
||||
spinnerPane.setContent(downloadButton);
|
||||
downloadButtonPane.getChildren().setAll(spinnerPane);
|
||||
|
||||
Consumer<DiscoJavaVersionList> updateListStatus = list -> {
|
||||
remoteVersionBox.setItems(null);
|
||||
packageTypeBox.getItems().clear();
|
||||
@@ -308,11 +313,11 @@ public final class JavaDownloadDialog extends StackPane {
|
||||
warningLabel.setText(null);
|
||||
|
||||
if (list == null || (list.versions != null && list.versions.isEmpty()))
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
spinnerPane.hideSpinner();
|
||||
else if (list.status == DiscoJavaVersionList.Status.LOADING)
|
||||
downloadButtonPane.getChildren().setAll(new JFXSpinner());
|
||||
spinnerPane.showSpinner();
|
||||
else {
|
||||
downloadButtonPane.getChildren().setAll(downloadButton);
|
||||
spinnerPane.hideSpinner();
|
||||
|
||||
if (list.status == DiscoJavaVersionList.Status.SUCCESS) {
|
||||
packageTypeBox.getItems().setAll(list.versions.keySet());
|
||||
|
||||
@@ -54,6 +54,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
@@ -98,7 +99,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
if (mainPage == null) {
|
||||
MainPage mainPage = new MainPage();
|
||||
FXUtils.applyDragListener(mainPage,
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file),
|
||||
file -> ModpackHelper.isFileModpackByExtension(file) || NBTFileType.isNBTFileByExtension(file) || "json".equalsIgnoreCase(FileUtils.getExtension(file)),
|
||||
modpacks -> {
|
||||
Path file = modpacks.get(0);
|
||||
if (ModpackHelper.isFileModpackByExtension(file)) {
|
||||
@@ -113,6 +114,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
Controllers.dialog(i18n("nbt.open.failed") + "\n\n" + StringUtils.getStackTrace(e),
|
||||
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
|
||||
Versions.installFromJson(Profiles.getSelectedProfile(), file);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ public class SkinCube extends MeshView {
|
||||
public static float[] createTexCoords(float width, float height, float depth, float scaleX, float scaleY,
|
||||
float startX, float startY, boolean isSlim) {
|
||||
float x = (width + depth) * 2, y = height + depth, half_width = width / x * scaleX, half_depth = depth / x * scaleX,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = isSlim ? half_depth : half_width,
|
||||
top_x = depth / x * scaleX + startX, top_y = startY, arm4 = half_width,
|
||||
bottom_x = startX, middle_y = depth / y * scaleY + top_y, bottom_y = scaleY + top_y;
|
||||
return new float[]{
|
||||
top_x, top_y, // T0 ---
|
||||
|
||||
@@ -25,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.game.GraphicsAPI;
|
||||
import org.jackhuang.hmcl.game.NativesDirectoryType;
|
||||
import org.jackhuang.hmcl.game.Renderer;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
@@ -69,6 +71,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
private final ComponentSublist nativesDirSublist;
|
||||
private final MultiFileItem<NativesDirectoryType> nativesDirItem;
|
||||
private final MultiFileItem.FileOption<NativesDirectoryType> nativesDirCustomOption;
|
||||
private final LineSelectButton<GraphicsAPI> graphicsBackendPane;
|
||||
private final LineSelectButton<Renderer> rendererPane;
|
||||
|
||||
public AdvancedVersionSettingPage(Profile profile, @Nullable String versionId, VersionSetting versionSetting) {
|
||||
@@ -79,6 +82,10 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
versionId == null ? i18n("settings.advanced") : i18n("settings.advanced.title", versionId)
|
||||
));
|
||||
|
||||
@Nullable GameVersionNumber gameVersion = versionId != null
|
||||
? GameVersionNumber.asGameVersion(profile.getRepository().getGameVersion(versionId))
|
||||
: null;
|
||||
|
||||
this.getStyleClass().add("gray-background");
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane();
|
||||
@@ -185,6 +192,25 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
nativesDirHint.setText(i18n("settings.advanced.natives_directory.hint"));
|
||||
nativesDirItem.getChildren().add(nativesDirHint);
|
||||
|
||||
graphicsBackendPane = new LineSelectButton<>();
|
||||
graphicsBackendPane.setTitle(i18n("settings.advanced.graphics_backend"));
|
||||
graphicsBackendPane.setConverter(backend -> i18n("settings.advanced.graphics_backend." + backend.name().toLowerCase(Locale.ROOT)));
|
||||
graphicsBackendPane.setDescriptionConverter(backend -> switch (backend) {
|
||||
case DEFAULT -> i18n("settings.advanced.graphics_backend.default.desc");
|
||||
case OPENGL -> i18n("settings.advanced.graphics_backend.opengl.desc");
|
||||
case VULKAN -> {
|
||||
if (gameVersion == null)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.global");
|
||||
else if (gameVersion.compareTo("26.2-snapshot-2") < 0)
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc.unsupported");
|
||||
else
|
||||
yield i18n("settings.advanced.graphics_backend.vulkan.desc");
|
||||
}
|
||||
default -> null;
|
||||
});
|
||||
graphicsBackendPane.setValue(GraphicsAPI.DEFAULT);
|
||||
graphicsBackendPane.setItems(GraphicsAPI.values());
|
||||
|
||||
rendererPane = new LineSelectButton<>();
|
||||
rendererPane.setTitle(i18n("settings.advanced.renderer"));
|
||||
rendererPane.setConverter(e -> i18n("settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT)));
|
||||
@@ -192,7 +218,23 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
String bundleKey = "settings.advanced.renderer." + e.name().toLowerCase(Locale.ROOT) + ".desc";
|
||||
return I18n.hasKey(bundleKey) ? i18n(bundleKey) : null;
|
||||
});
|
||||
rendererPane.setItems(Renderer.values());
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
|
||||
FXUtils.onChangeAndOperate(graphicsBackendPane.valueProperty(), backend -> {
|
||||
if (backend == null) { // unbind
|
||||
return;
|
||||
}
|
||||
|
||||
rendererPane.setItems(Renderer.getSupported(backend));
|
||||
if (backend == GraphicsAPI.DEFAULT) {
|
||||
rendererPane.setDisable(true);
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
} else {
|
||||
rendererPane.setDisable(false);
|
||||
if (!(rendererPane.getValue() instanceof Renderer.Driver driver) || driver.api() != backend)
|
||||
rendererPane.setValue(Renderer.DEFAULT);
|
||||
}
|
||||
});
|
||||
|
||||
noJVMArgsPane = new LineToggleButton();
|
||||
noJVMArgsPane.setTitle(i18n("settings.advanced.no_jvm_args"));
|
||||
@@ -219,7 +261,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
useNativeOpenALPane.setSubtitle(i18n("settings.advanced.linux_freebsd_only"));
|
||||
|
||||
workaroundPane.getContent().setAll(
|
||||
nativesDirSublist, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
nativesDirSublist, graphicsBackendPane, rendererPane, noJVMArgsPane, noOptimizingJVMArgsPane, noGameCheckPane,
|
||||
noJVMCheckPane, noNativesPatchPane
|
||||
);
|
||||
|
||||
@@ -252,6 +294,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.bindString(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.bindString(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().bindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().bindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().bindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().bindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
@@ -294,6 +337,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
|
||||
FXUtils.unbind(txtPreLaunchCommand, versionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, versionSetting.postExitCommandProperty());
|
||||
rendererPane.valueProperty().unbindBidirectional(versionSetting.rendererProperty());
|
||||
graphicsBackendPane.valueProperty().unbindBidirectional(versionSetting.graphicsBackendProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckGameProperty());
|
||||
noJVMCheckPane.selectedProperty().unbindBidirectional(versionSetting.notCheckJVMProperty());
|
||||
noJVMArgsPane.selectedProperty().unbindBidirectional(versionSetting.noJVMArgsProperty());
|
||||
|
||||
@@ -225,8 +225,10 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
content.setSubtitle(getSkinnable().addon.getDescription());
|
||||
content.getSubtitleLabel().setWrapText(true);
|
||||
getSkinnable().addon.getCategories().stream()
|
||||
.filter(category -> getSkinnable().page.shouldDisplayCategory(category))
|
||||
.map(category -> getSkinnable().page.getLocalizedCategory(category))
|
||||
.forEach(content::addTag);
|
||||
content.getFirstLine().setMinWidth(0);
|
||||
descriptionPane.getChildren().add(content);
|
||||
|
||||
if (getSkinnable().mod != null) {
|
||||
@@ -273,7 +275,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
Version game = repository.getResolvedPreservingPatchesVersion(control.version.getVersion());
|
||||
String gameVersion = repository.getGameVersion(game).orElse(null);
|
||||
|
||||
if (gameVersion != null) {
|
||||
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
|
||||
List<RemoteMod.Version> modVersions = control.versions.get(gameVersion);
|
||||
if (modVersions != null && !modVersions.isEmpty()) {
|
||||
Set<ModLoaderType> targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders();
|
||||
@@ -351,7 +353,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DependencyModItem extends StackPane {
|
||||
private static final class DependencyModItem extends LineButton {
|
||||
public static final EnumMap<RemoteMod.DependencyType, String> I18N_KEY = new EnumMap<>(Lang.mapOf(
|
||||
Pair.pair(RemoteMod.DependencyType.EMBEDDED, "mods.dependency.embedded"),
|
||||
Pair.pair(RemoteMod.DependencyType.OPTIONAL, "mods.dependency.optional"),
|
||||
@@ -367,16 +369,16 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
pane.setPadding(new Insets(0, 8, 0, 8));
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
pane.setMouseTransparent(true);
|
||||
HBox.setHgrow(content, Priority.ALWAYS);
|
||||
var imageView = new ImageContainer(40);
|
||||
pane.getChildren().setAll(imageView, content);
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
FXUtils.onClicked(container, () -> {
|
||||
FXUtils.setLimitHeight(this, 60);
|
||||
setOnAction((e) -> {
|
||||
fireEvent(new DialogCloseEvent());
|
||||
Controllers.navigate(new DownloadPage(page, addon, version, callback));
|
||||
});
|
||||
getChildren().setAll(container);
|
||||
setNode(IDX_LEADING, pane);
|
||||
|
||||
if (addon != RemoteMod.BROKEN) {
|
||||
ModTranslations.Mod mod = ModTranslations.getTranslationsByRepositoryType(page.repository.getType()).getModByCurseForgeId(addon.getSlug());
|
||||
@@ -493,6 +495,7 @@ public class DownloadPage extends Control implements DecoratorPage {
|
||||
loadDependencies(version, selfPage, spinnerPane, dependenciesList);
|
||||
spinnerPane.setOnFailedAction(e -> loadDependencies(version, selfPage, spinnerPane, dependenciesList));
|
||||
|
||||
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scrollPane.setContent(dependenciesList);
|
||||
scrollPane.setFitToWidth(true);
|
||||
scrollPane.setFitToHeight(true);
|
||||
|
||||
@@ -23,7 +23,10 @@ import com.jfoenix.controls.JFXListView;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
@@ -157,20 +160,9 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
return;
|
||||
|
||||
Path parent = currentDirectory.path;
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
Controllers.prompt(
|
||||
i18n("schematics.create_directory.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = parent.resolve(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("schematics.create_directory.failed.already_exists"));
|
||||
@@ -185,7 +177,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
|
||||
LOG.warning("Failed to create directory: " + targetDir, e);
|
||||
handler.reject(i18n("schematics.create_directory.failed", targetDir));
|
||||
}
|
||||
}));
|
||||
}, "", new RequiredValidator(), new Validator(i18n("schematics.create_directory.failed.invalid_name"), FileUtils::isNameValid));
|
||||
}
|
||||
|
||||
private DirItem loadAll(Path dir, @Nullable DirItem parent) {
|
||||
|
||||
@@ -22,8 +22,11 @@ import javafx.application.Platform;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
import org.jackhuang.hmcl.setting.*;
|
||||
@@ -146,7 +149,8 @@ public final class Versions {
|
||||
} else {
|
||||
handler.reject(i18n("version.manage.rename.fail"));
|
||||
}
|
||||
}, version, new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
}, version,
|
||||
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
|
||||
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
|
||||
}
|
||||
|
||||
@@ -158,6 +162,43 @@ public final class Versions {
|
||||
FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
|
||||
}
|
||||
|
||||
public static void installFromJson(Profile profile, Path file) {
|
||||
Version version;
|
||||
try {
|
||||
version = profile.getRepository().readVersionJson(file);
|
||||
} catch (Exception e) {
|
||||
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
|
||||
handler.resolve();
|
||||
|
||||
DefaultDependencyManager dependencyManager = profile.getDependency();
|
||||
HMCLGameRepository repository = profile.getRepository();
|
||||
Version newVersion = version.setId(result).setJar(result);
|
||||
|
||||
Controllers.taskDialog(
|
||||
Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion),
|
||||
Task.allOf(
|
||||
new GameAssetDownloadTask(dependencyManager, newVersion, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true),
|
||||
new GameLibrariesTask(dependencyManager, newVersion, true)
|
||||
).withRunAsync(() -> {
|
||||
// ignore failure
|
||||
}))
|
||||
.thenComposeAsync(repository.saveAsync(newVersion))
|
||||
.thenRunAsync(repository::refreshVersions)
|
||||
.whenComplete(Schedulers.javafx(), (exception) -> {
|
||||
if (exception == null) {
|
||||
profile.setSelectedVersion(result);
|
||||
} else {
|
||||
Controllers.dialog(
|
||||
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
|
||||
}
|
||||
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
|
||||
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)));
|
||||
}
|
||||
|
||||
public static void duplicateVersion(Profile profile, String version) {
|
||||
Controllers.prompt(
|
||||
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
|
||||
|
||||
@@ -23,8 +23,9 @@ import org.jackhuang.hmcl.game.WorldLockedException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.construct.InputDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
|
||||
import org.jackhuang.hmcl.ui.construct.Validator;
|
||||
import org.jackhuang.hmcl.ui.wizard.SinglePageWizardProvider;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
@@ -89,20 +90,9 @@ public final class WorldManageUIUtils {
|
||||
|
||||
public static void copyWorld(World world, Runnable runnable) {
|
||||
Path worldPath = world.getFile();
|
||||
Controllers.dialog(new InputDialogPane(
|
||||
Controllers.prompt(
|
||||
i18n("world.duplicate.prompt"),
|
||||
"",
|
||||
(result, handler) -> {
|
||||
if (StringUtils.isBlank(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.empty_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.contains("/") || result.contains("\\") || !FileUtils.isNameValid(result)) {
|
||||
handler.reject(i18n("world.duplicate.failed.invalid_name"));
|
||||
return;
|
||||
}
|
||||
|
||||
Path targetDir = worldPath.resolveSibling(result);
|
||||
if (Files.exists(targetDir)) {
|
||||
handler.reject(i18n("world.duplicate.failed.already_exists"));
|
||||
@@ -125,7 +115,7 @@ public final class WorldManageUIUtils {
|
||||
}
|
||||
})
|
||||
.start();
|
||||
}));
|
||||
}, "", new RequiredValidator(), new Validator(i18n("world.duplicate.failed.invalid_name"), FileUtils::isNameValid));
|
||||
}
|
||||
|
||||
public static void closeSessionLockChannel(World world, FileChannel sessionLockChannel) throws IOException {
|
||||
|
||||
@@ -189,15 +189,16 @@ public final class NativePatcher {
|
||||
return version.setLibraries(newLibraries);
|
||||
}
|
||||
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime javaVersion, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
/// @see <a href="https://github.com/HMCL-dev/mesa-loader-windows">Java Mesa Loader for Windows</a>
|
||||
public static @Nullable Library getWindowsMesaLoader(@NotNull JavaRuntime java, @NotNull Renderer renderer, @NotNull OSVersion windowsVersion) {
|
||||
if (renderer == Renderer.DEFAULT)
|
||||
return null;
|
||||
|
||||
if (windowsVersion.isAtLeast(OSVersion.WINDOWS_10)) {
|
||||
return getNatives(javaVersion.getPlatform()).get("mesa-loader");
|
||||
return getNatives(java.getPlatform()).get("mesa-loader");
|
||||
} else if (windowsVersion.isAtLeast(OSVersion.WINDOWS_7)) {
|
||||
if (renderer == Renderer.LLVMPIPE)
|
||||
return getNatives(javaVersion.getPlatform()).get("software-renderer-loader");
|
||||
if (renderer == Renderer.OpenGL.LLVMPIPE)
|
||||
return getNatives(java.getPlatform()).get("software-renderer-loader");
|
||||
else
|
||||
return null;
|
||||
} else {
|
||||
|
||||
@@ -96,6 +96,10 @@ public class Translator_lzh extends Translator {
|
||||
case GA -> {
|
||||
// do nothing
|
||||
}
|
||||
case SNAPSHOT -> {
|
||||
builder.append("之快照");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
}
|
||||
case PRE_RELEASE -> {
|
||||
builder.append("之預");
|
||||
appendDigitByDigit(builder, release.getEaVersion().toString());
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
{
|
||||
"title" : "HelloNBT",
|
||||
"subtitle" : "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink" : "https://github.com/Glavo/HelloNBT"
|
||||
"externalLink" : "https://github.com/HMCL-dev/HelloNBT"
|
||||
},
|
||||
{
|
||||
"title" : "minecraft-jfx-skin",
|
||||
@@ -88,5 +88,10 @@
|
||||
"title": "MonetFX",
|
||||
"subtitle": "Copyright © 2025 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/Glavo/MonetFX"
|
||||
},
|
||||
{
|
||||
"title": "LWJGL Unsafe Agent",
|
||||
"subtitle": "Copyright © 2026 Glavo.\nLicensed under the Apache 2.0 License.",
|
||||
"externalLink": "https://github.com/HMCL-dev/lwjgl-unsafe-agent"
|
||||
}
|
||||
]
|
||||
@@ -414,7 +414,7 @@
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-name {
|
||||
-fx-pref-width: 80px;
|
||||
-fx-pref-width: 100px;
|
||||
}
|
||||
|
||||
.installer-item-wrapper .installer-item:list-item > .installer-item-status {
|
||||
@@ -1482,6 +1482,11 @@
|
||||
-fx-max-width: 1000000000;
|
||||
}
|
||||
|
||||
.jfx-combo-box .arrow {
|
||||
-fx-background-color: -monet-on-surface-variant;
|
||||
-fx-shape: "M12 15 7 10H17L12 15Z";
|
||||
}
|
||||
|
||||
.jfx-combo-box .text {
|
||||
-fx-fill: -monet-on-surface;
|
||||
}
|
||||
@@ -1497,6 +1502,7 @@
|
||||
.combo-box-popup .list-view .jfx-list-cell {
|
||||
-fx-background-color: -monet-surface-container;
|
||||
-fx-text-fill: -monet-on-surface;
|
||||
-fx-cursor: hand;
|
||||
-fx-background-insets: 0.0;
|
||||
}
|
||||
|
||||
@@ -1506,6 +1512,10 @@
|
||||
-fx-text-fill: -monet-on-secondary-container;
|
||||
}
|
||||
|
||||
.combo-box-popup .list-view .list-cell:hover > .jfx-rippler > StackPane {
|
||||
-fx-background-color: -monet-secondary-container-transparent-50;
|
||||
}
|
||||
|
||||
.jfx-combo-box-warning {
|
||||
-jfx-focus-color: #D34336;
|
||||
-jfx-unfocus-color: #D34336;
|
||||
|
||||
@@ -762,6 +762,7 @@ install.new_game.already_exists=This instance name already exists. Please use an
|
||||
install.new_game.current_game_version=Current Instance Version
|
||||
install.new_game.installation=Instance Installation
|
||||
install.new_game.malformed=Invalid name.
|
||||
install.new_game.malformed_json=Invalid Minecraft instance JSON file.
|
||||
install.select=Choose operation
|
||||
install.success=Successfully installed.
|
||||
|
||||
@@ -1231,7 +1232,7 @@ world.load.fail=Failed to load world
|
||||
world.locked=In use
|
||||
world.locked.failed=The world is currently in use. Please close the game and try again.
|
||||
world.manage=Worlds
|
||||
world.manage.button=World Management
|
||||
world.manage.button=Manage World
|
||||
world.manage.title=World - %s
|
||||
world.name=World Name
|
||||
world.name.enter=Enter the world name
|
||||
@@ -1336,6 +1337,15 @@ settings.advanced.dont_patch_natives=Do not attempt to automatically replace nat
|
||||
settings.advanced.environment_variables=Environment Variables
|
||||
settings.advanced.game_dir.default=Default (".minecraft/")
|
||||
settings.advanced.game_dir.independent=Isolated (".minecraft/versions/<instance name>/", except for assets and libraries)
|
||||
settings.advanced.graphics_backend=Graphics API
|
||||
settings.advanced.graphics_backend.default=Default
|
||||
settings.advanced.graphics_backend.default.desc=Follow the game's settings
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=Use OpenGL for rendering
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=Use Vulkan for rendering
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=Use Vulkan for rendering (Since Minecraft 26.2)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=Use Vulkan for rendering (not supported for the current game version)
|
||||
settings.advanced.java_permanent_generation_space=PermGen Space
|
||||
settings.advanced.java_permanent_generation_space.prompt=in MiB
|
||||
settings.advanced.jvm=JVM Options
|
||||
@@ -1378,13 +1388,47 @@ settings.advanced.post_exit_command=Post-exit Command
|
||||
settings.advanced.post_exit_command.prompt=Commands to execute after the game exits
|
||||
settings.advanced.renderer=Renderer
|
||||
settings.advanced.renderer.default=Default
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Poor performance and compatibility)
|
||||
settings.advanced.renderer.default.desc=Use System Default
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=Software Vulkan Renderer
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=Vulkan renderer based on DirectX 12 (Experimental)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=Official Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=Open-source Vulkan driver for NVIDIA GPUs
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=Official Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=Open-source Vulkan driver for AMD GPUs
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Official Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Open-source Vulkan driver for Intel GPUs
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Open-source Vulkan driver for older Intel GPUs
|
||||
settings.advanced.renderer.qualcomm=Qualcomm
|
||||
settings.advanced.renderer.qualcomm.desc=Official Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.turnip=Qualcomm (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=Open-source Vulkan driver for Qualcomm Adreno GPUs
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=Vulkan renderer based on Metal
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=Vulkan renderer based on Metal 4 (Experimental)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=Open-source Vulkan driver for PowerVR GPUs (Experimental)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Open-source Vulkan driver for Arm Mali GPUs (Experimental)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=Open-source Vulkan driver for Raspberry Pi 4/5 GPUs (Experimental)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Poor performance, best compatibility)
|
||||
settings.advanced.renderer.llvmpipe.desc=Software OpenGL renderer
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=OpenGL renderer based on DirectX 12
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Best performance, poor compatibility)
|
||||
settings.advanced.renderer.zink.desc=OpenGL renderer based on Vulkan
|
||||
settings.advanced.server_ip=Server Address
|
||||
settings.advanced.server_ip.prompt=Automatically join after launching the game
|
||||
settings.advanced.unsupported_system_options=Settings not applicable to the current system
|
||||
|
||||
@@ -1218,7 +1218,6 @@ world.load.fail=فشل تحميل العالم
|
||||
world.locked=قيد الاستخدام
|
||||
world.locked.failed=العالم قيد الاستخدام حالياً. يُرجى إغلاق اللعبة والمحاولة مجدداً.
|
||||
world.manage=العوالم
|
||||
world.manage.button=إدارة العوالم
|
||||
world.manage.title=العالم - %s
|
||||
world.name=اسم العالم
|
||||
world.name.enter=أدخل اسم العالم
|
||||
@@ -1365,13 +1364,9 @@ settings.advanced.post_exit_command=أمر ما بعد الإغلاق
|
||||
settings.advanced.post_exit_command.prompt=أوامر تُنفَّذ بعد إغلاق اللعبة
|
||||
settings.advanced.renderer=المُصيِّر
|
||||
settings.advanced.renderer.default=افتراضي
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (أداء وتوافق ضعيفان)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=برمجي (أداء ضعيف، توافق أفضل)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (أفضل أداء، توافق ضعيف)
|
||||
settings.advanced.server_ip=عنوان الخادم
|
||||
settings.advanced.server_ip.prompt=الانضمام تلقائياً بعد تشغيل اللعبة
|
||||
settings.advanced.unsupported_system_options=إعدادات غير مطبّقة على النظام الحالي
|
||||
|
||||
@@ -1138,7 +1138,6 @@ world.info.random_seed=Semilla
|
||||
world.locked=En uso
|
||||
world.locked.failed=El mundo está actualmente en uso. Por favor, cierra el juego e inténtalo de nuevo.
|
||||
world.manage=Mundos
|
||||
world.manage.button=Administrar
|
||||
world.manage.title=Mundo - %s
|
||||
world.name=Nombre del mundo
|
||||
world.name.enter=Introducir el nombre del mundo
|
||||
@@ -1269,13 +1268,9 @@ settings.advanced.post_exit_command=Comando post-salida
|
||||
settings.advanced.post_exit_command.prompt=El comando se ejecuta después de que el juego se detenga
|
||||
settings.advanced.renderer=Renderizador
|
||||
settings.advanced.renderer.default=Por defecto
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Rendimiento y compatibilidad deficientes)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Software (Bajo rendimiento, máxima compatibilidad)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Máximo rendimiento, baja compatibilidad)
|
||||
settings.advanced.server_ip=Dirección del servidor
|
||||
settings.advanced.server_ip.prompt=Entrar automáticamente después de ejecutar el juego
|
||||
settings.advanced.unsupported_system_options=Configuración no aplicable al sistema actual
|
||||
|
||||
@@ -792,13 +792,9 @@ settings.advanced.post_exit_command=終了後のコマンド
|
||||
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
|
||||
settings.advanced.renderer=レンダラー
|
||||
settings.advanced.renderer.default=デフォルト
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (低パフォーマンス、低互換性)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=ソフトウェア (低速だが、互換性は最高)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (最高パフォーマンス、低互換性)
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.unsupported_system_options=サポートされていないシステムオプション
|
||||
|
||||
@@ -951,8 +951,8 @@ world.info.player.xp_level=經驗之層
|
||||
world.info.random_seed=種
|
||||
world.locked=見用
|
||||
world.manage=司生界
|
||||
world.manage.button=司生界
|
||||
world.manage.title=司生界 - %s
|
||||
world.manage.button=司此生界
|
||||
world.manage.title=生界 - %s
|
||||
world.name=生界之名
|
||||
world.name.enter=書生界之名
|
||||
world.show_all=全顯
|
||||
@@ -1085,13 +1085,9 @@ settings.advanced.post_exit_command=令於戲訖
|
||||
settings.advanced.post_exit_command.prompt=將後戲訖而行
|
||||
settings.advanced.renderer=繪器
|
||||
settings.advanced.renderer.default=本
|
||||
settings.advanced.renderer.default.desc=開圖庫
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (效與所適皆差,勘誤之用)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=軟繪器 (效差,所適至)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (效至,所適差)
|
||||
settings.advanced.server_ip=伺服器之址
|
||||
settings.advanced.server_ip.prompt=本之址,啟訖徑入是伺服器
|
||||
settings.advanced.unsupported_system_options=不逮今之械網所選
|
||||
|
||||
@@ -1130,7 +1130,6 @@ world.info.random_seed=Ключ генератора мира
|
||||
world.locked=В эксплуатации
|
||||
world.locked.failed=В настоящее время мир находится в эксплуатации. Закройте игру и попробуйте снова.
|
||||
world.manage=Миры
|
||||
world.manage.button=Управлять
|
||||
world.manage.title=Мир - %s
|
||||
world.name=Название мира
|
||||
world.name.enter=Введите название мира
|
||||
@@ -1260,13 +1259,9 @@ settings.advanced.post_exit_command=Команда после выхода
|
||||
settings.advanced.post_exit_command.prompt=Команды, которые необходимо выполнить после выхода из игры
|
||||
settings.advanced.renderer=Рендерер
|
||||
settings.advanced.renderer.default=По умолчанию
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Низкая производительность и совместимость)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=ПО (Низкая производительность, лучшая совместимость)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Лучшая производительность, низкая совместимость)
|
||||
settings.advanced.server_ip=Адрес сервера
|
||||
settings.advanced.server_ip.prompt=Присоединяться к серверу при запуске игры
|
||||
settings.advanced.unsupported_system_options=Настройки, не применимые к текущей системе
|
||||
|
||||
@@ -1078,7 +1078,6 @@ world.info.random_seed=Насіння
|
||||
world.locked=Використовується
|
||||
world.locked.failed=Світ наразі використовується. Закрийте гру та спробуйте знову.
|
||||
world.manage=Світи
|
||||
world.manage.button=Керування світами
|
||||
world.manage.title=Світ - %s
|
||||
world.name=Назва світу
|
||||
world.name.enter=Введіть назву світу
|
||||
@@ -1207,13 +1206,9 @@ settings.advanced.post_exit_command=Команда після виходу
|
||||
settings.advanced.post_exit_command.prompt=Команди для виконання після виходу з гри
|
||||
settings.advanced.renderer=Рендерер
|
||||
settings.advanced.renderer.default=По умолчанию
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (Погана продуктивність та сумісність)
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=Програмний (Погана продуктивність, найкраща сумісність)
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (Найкраща продуктивність, погана сумісність)
|
||||
settings.advanced.server_ip=Адреса сервера
|
||||
settings.advanced.server_ip.prompt=Автоматично приєднатися після запуску гри
|
||||
settings.advanced.unsupported_system_options=Налаштування, що не застосовуються до поточної системи
|
||||
|
||||
@@ -566,6 +566,7 @@ install.new_game.already_exists=此實例已經存在,請重新命名
|
||||
install.new_game.current_game_version=目前遊戲實例
|
||||
install.new_game.installation=安裝新實例
|
||||
install.new_game.malformed=名稱無效
|
||||
install.new_game.malformed_json=無效的 Minecraft 實例 JSON 文件。
|
||||
install.select=請選取安裝方式
|
||||
install.success=安裝成功
|
||||
|
||||
@@ -1025,8 +1026,8 @@ world.locked=使用中
|
||||
world.locked.failed=該世界正在使用中,請關閉遊戲後重試。
|
||||
world.game_version=遊戲版本
|
||||
world.manage=世界管理
|
||||
world.manage.button=世界管理
|
||||
world.manage.title=世界管理 - %s
|
||||
world.manage.button=管理此世界
|
||||
world.manage.title=世界 - %s
|
||||
world.name=世界名稱
|
||||
world.name.enter=輸入世界名稱
|
||||
world.show_all=全部顯示
|
||||
@@ -1126,6 +1127,15 @@ settings.advanced.dont_patch_natives=不嘗試自動取代本機庫
|
||||
settings.advanced.environment_variables=環境變數
|
||||
settings.advanced.game_dir.default=預設 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各實例獨立 (".minecraft/versions/<實例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics_backend=圖形 API
|
||||
settings.advanced.graphics_backend.default=預設
|
||||
settings.advanced.graphics_backend.default.desc=遵循遊戲設定
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (適用於 Minecraft 26.2 及更高版本)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不適用於當前遊戲版本)
|
||||
settings.advanced.java_permanent_generation_space=記憶體永久儲存區域
|
||||
settings.advanced.java_permanent_generation_space.prompt=單位 MiB
|
||||
settings.advanced.jvm=Java 虛擬機設定
|
||||
@@ -1163,15 +1173,49 @@ settings.advanced.process_priority.high=高
|
||||
settings.advanced.process_priority.high.desc=優先保證遊戲執行,但可能會導致其他程式卡頓
|
||||
settings.advanced.post_exit_command=遊戲結束後執行指令
|
||||
settings.advanced.post_exit_command.prompt=將在遊戲結束後呼叫使用
|
||||
settings.advanced.renderer=繪製器
|
||||
settings.advanced.renderer=繪製器/驅動
|
||||
settings.advanced.renderer.default=預設
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (效能與相容性較差,用於除錯)
|
||||
settings.advanced.renderer.default.desc=使用系統預設繪製器
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=軟體 Vulkan 繪製器
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=基於 DirectX 12 的 Vulkan 繪製器 (實驗性)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=AMD GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Intel GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Intel 舊 GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.qualcomm=高通
|
||||
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驅動
|
||||
settings.advanced.renderer.turnip=高通 (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的開源 Vulkan 驅動
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=基於 Metal 的 Vulkan 繪製器
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=基於 Metal 4 的 Vulkan 繪製器 (實驗性)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=PowerVR GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=樹莓派 4/5 GPU 的開源 Vulkan 驅動 (實驗性)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=軟繪製器 (效能較差,相容性最好)
|
||||
settings.advanced.renderer.llvmpipe.desc=軟體 OpenGL 繪製器
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=基於 DirectX 12 的 OpenGL 繪製器
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (效能最好,相容性較差)
|
||||
settings.advanced.renderer.zink.desc=基於 Vulkan 的 OpenGL 繪製器
|
||||
settings.advanced.server_ip=伺服器位址
|
||||
settings.advanced.server_ip.prompt=預設,啟動遊戲後直接進入對應伺服器
|
||||
settings.advanced.unsupported_system_options=不適用於目前系統的選項
|
||||
|
||||
@@ -571,6 +571,7 @@ install.new_game.already_exists=此实例已经存在,请换一个名字
|
||||
install.new_game.current_game_version=当前游戏实例
|
||||
install.new_game.installation=安装新游戏
|
||||
install.new_game.malformed=名字不合法
|
||||
install.new_game.malformed_json=不是有效的 Minecraft 实例 JSON 文件。
|
||||
install.select=请选择安装方式
|
||||
install.success=安装成功
|
||||
|
||||
@@ -1030,8 +1031,8 @@ world.load.fail=世界加载失败
|
||||
world.locked=使用中
|
||||
world.locked.failed=该世界正在使用中,请关闭游戏后重试。
|
||||
world.manage=世界管理
|
||||
world.manage.button=世界管理
|
||||
world.manage.title=世界管理 - %s
|
||||
world.manage.button=管理此世界
|
||||
world.manage.title=世界 - %s
|
||||
world.name=世界名称
|
||||
world.name.enter=输入世界名称
|
||||
world.show_all=显示全部
|
||||
@@ -1131,6 +1132,15 @@ settings.advanced.dont_patch_natives=不尝试自动替换本地库
|
||||
settings.advanced.environment_variables=环境变量
|
||||
settings.advanced.game_dir.default=默认 (".minecraft/")
|
||||
settings.advanced.game_dir.independent=各实例独立 (存放在 ".minecraft/versions/<实例名>/",除 assets、libraries 外)
|
||||
settings.advanced.graphics_backend=图形 API
|
||||
settings.advanced.graphics_backend.default=默认
|
||||
settings.advanced.graphics_backend.default.desc=遵循游戏设置
|
||||
settings.advanced.graphics_backend.opengl=OpenGL
|
||||
settings.advanced.graphics_backend.opengl.desc=使用 OpenGL 渲染
|
||||
settings.advanced.graphics_backend.vulkan=Vulkan
|
||||
settings.advanced.graphics_backend.vulkan.desc=使用 Vulkan 渲染
|
||||
settings.advanced.graphics_backend.vulkan.desc.global=使用 Vulkan 渲染 (适用于 Minecraft 26.2 及更高版本)
|
||||
settings.advanced.graphics_backend.vulkan.desc.unsupported=使用 Vulkan 渲染 (不适用于当前游戏版本)
|
||||
settings.advanced.java_permanent_generation_space=内存永久保存区域
|
||||
settings.advanced.java_permanent_generation_space.prompt=单位 MiB
|
||||
settings.advanced.jvm=Java 虚拟机设置
|
||||
@@ -1168,15 +1178,49 @@ settings.advanced.process_priority.high=高
|
||||
settings.advanced.process_priority.high.desc=优先保证游戏运行,但可能会导致其他程序卡顿
|
||||
settings.advanced.post_exit_command=游戏结束后执行命令
|
||||
settings.advanced.post_exit_command.prompt=将在游戏结束后调用
|
||||
settings.advanced.renderer=渲染器
|
||||
settings.advanced.renderer=渲染器/驱动
|
||||
settings.advanced.renderer.default=默认
|
||||
settings.advanced.renderer.default.desc=OpenGL
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=DirectX 12 (性能与兼容性较差,用于调试)
|
||||
settings.advanced.renderer.default.desc=使用系统默认渲染器
|
||||
# Vulkan Renderers
|
||||
settings.advanced.renderer.lavapipe=Mesa Lavapipe
|
||||
settings.advanced.renderer.lavapipe.desc=软件 Vulkan 渲染器
|
||||
settings.advanced.renderer.dozen=Mesa Dozen
|
||||
settings.advanced.renderer.dozen.desc=基于 DirectX 12 的 Vulkan 渲染器 (实验性)
|
||||
settings.advanced.renderer.nvidia_vulkan=NVIDIA
|
||||
settings.advanced.renderer.nvidia_vulkan.desc=NVIDIA GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.nvidia_nvk=NVIDIA (NVK)
|
||||
settings.advanced.renderer.nvidia_nvk.desc=NVIDIA GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.amdvlk=AMD
|
||||
settings.advanced.renderer.amdvlk.desc=AMD GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.amd_radv=AMD (RADV)
|
||||
settings.advanced.renderer.amd_radv.desc=AMD GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_vulkan=Intel
|
||||
settings.advanced.renderer.intel_vulkan.desc=Intel GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_anv=Intel (ANV)
|
||||
settings.advanced.renderer.intel_anv.desc=Intel GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.intel_hasvk=Intel (HASVK)
|
||||
settings.advanced.renderer.intel_hasvk.desc=Intel 旧 GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.qualcomm=高通
|
||||
settings.advanced.renderer.qualcomm.desc=高通 Adreno GPU 的官方 Vulkan 驱动
|
||||
settings.advanced.renderer.turnip=高通 (Turnip)
|
||||
settings.advanced.renderer.turnip.desc=高通 Adreno GPU 的开源 Vulkan 驱动
|
||||
settings.advanced.renderer.moltenvk=MoltenVK
|
||||
settings.advanced.renderer.moltenvk.desc=基于 Metal 的 Vulkan 渲染器
|
||||
settings.advanced.renderer.kosmickrisp=KosmicKrisp
|
||||
settings.advanced.renderer.kosmickrisp.desc=基于 Metal 4 的 Vulkan 渲染器 (实验性)
|
||||
settings.advanced.renderer.powervr=PowerVR
|
||||
settings.advanced.renderer.powervr.desc=PowerVR GPU 的开源 Vulkan 驱动 (实验性)
|
||||
settings.advanced.renderer.panvk=PanVK
|
||||
settings.advanced.renderer.panvk.desc=Arm Mali GPU 的开源 Vulkan 驱动 (实验性)
|
||||
settings.advanced.renderer.v3dv=V3DV
|
||||
settings.advanced.renderer.v3dv.desc=树莓派 4/5 GPU 的开源 Vulkan 驱动 (实验性)
|
||||
# OpenGL Renderers
|
||||
settings.advanced.renderer.llvmpipe=Mesa LLVMpipe
|
||||
settings.advanced.renderer.llvmpipe.desc=软渲染器 (性能较差,兼容性最好)
|
||||
settings.advanced.renderer.llvmpipe.desc=软件 OpenGL 渲染器
|
||||
settings.advanced.renderer.d3d12=Mesa D3D12
|
||||
settings.advanced.renderer.d3d12.desc=基于 DirectX 12 的 OpenGL 渲染器
|
||||
settings.advanced.renderer.zink=Mesa Zink
|
||||
settings.advanced.renderer.zink.desc=Vulkan (性能最好,兼容性较差)
|
||||
settings.advanced.renderer.zink.desc=基于 Vulkan 的 OpenGL 渲染器
|
||||
settings.advanced.server_ip=服务器地址
|
||||
settings.advanced.server_ip.prompt=默认,启动游戏后可以直接进入对应服务器
|
||||
settings.advanced.unsupported_system_options=不适用于当前系统的选项
|
||||
|
||||
@@ -781,17 +781,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl/3.4.1/lwjgl-3.4.1.jar",
|
||||
"sha1": "6105690714874b995aa7812e4f7732a21109276c",
|
||||
"size": 1157957
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -803,17 +792,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-jemalloc/3.4.1/lwjgl-jemalloc-3.4.1.jar",
|
||||
"sha1": "16b663092854fc6adfe0a941e61cae2bb2e437b6",
|
||||
"size": 47872
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-jemalloc:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -825,17 +803,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-openal/3.4.1/lwjgl-openal-3.4.1.jar",
|
||||
"sha1": "8508bd60589de0539aa465b78fcf838efd07ae9a",
|
||||
"size": 153919
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-openal:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-openal:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -847,17 +814,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-opengl/3.4.1/lwjgl-opengl-3.4.1.jar",
|
||||
"sha1": "830412cab823c029cda6b9729f9d2ed36cf79959",
|
||||
"size": 937660
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-opengl:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-opengl:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -869,17 +825,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-glfw/3.4.1/lwjgl-glfw-3.4.1.jar",
|
||||
"sha1": "a782b1ddd175c9107bf300fd92920579ea65e2a4",
|
||||
"size": 151893
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-glfw:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-glfw:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -891,17 +836,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-stb/3.4.1/lwjgl-stb-3.4.1.jar",
|
||||
"sha1": "f077c3dafc31924fbe8acb0b913f08977ba27f8e",
|
||||
"size": 142927
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-stb:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -913,17 +847,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-tinyfd/3.4.1/lwjgl-tinyfd-3.4.1.jar",
|
||||
"sha1": "562697d40dbd2ed0424bf24dcf65d51170f81adf",
|
||||
"size": 15931
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -935,17 +858,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-freetype/3.4.1/lwjgl-freetype-3.4.1.jar",
|
||||
"sha1": "caa75a37e857efc9b0686e434d708d6420c5b6ea",
|
||||
"size": 464342
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-freetype:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
@@ -957,6 +869,39 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-arm64.jar",
|
||||
"sha1": "bc5c99b2eb7e2109c6db21cfced15a6851f8111b",
|
||||
"size": 3397476
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-arm64.jar",
|
||||
"sha1": "e2eaa8f516d43fe11b8253e6243b3d8e1315c9c8",
|
||||
"size": 1139919
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-arm64.jar",
|
||||
"sha1": "de40d43c5947c4a4f91376af8f2c00e5396cc109",
|
||||
"size": 59715
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl.lwjgl:lwjgl-platform:2.9.0:natives": {
|
||||
"name": "org.glavo.hmcl:lwjgl2-natives:2.9.3",
|
||||
"downloads": {
|
||||
@@ -2689,6 +2634,9 @@
|
||||
"org.lwjgl:lwjgl-stb:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-tinyfd:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-freetype:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": null,
|
||||
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": null,
|
||||
"net.java.dev.jna:jna:5.8.0": {
|
||||
"name": "net.java.dev.jna:jna:5.13.0",
|
||||
"downloads": {
|
||||
@@ -4359,6 +4307,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
|
||||
"size": 153051
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-linux-riscv64.jar",
|
||||
"sha1": "5598b0b86d4b3ca0ae78817719d5814efb9c7a77",
|
||||
"size": 3147886
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
|
||||
"size": 148285
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-linux-riscv64.jar",
|
||||
"sha1": "ea09f614c961b14962e4cc55a05c6767f9440f3f",
|
||||
"size": 1215586
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
|
||||
"size": 109209
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-linux-riscv64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-linux-riscv64.jar",
|
||||
"sha1": "9616440ee3cb2cfeacd99a61eefded856b5d7efe",
|
||||
"size": 57091
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vulkan:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
|
||||
"size": 8356700
|
||||
}
|
||||
}
|
||||
},
|
||||
"com.github.oshi:oshi-core:6.6.5": {
|
||||
"name": "com.github.oshi:oshi-core:6.8.0",
|
||||
"downloads": {
|
||||
@@ -4378,13 +4403,13 @@
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:x64",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:x64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x64.jar",
|
||||
"sha1": "b2552fcc8c98e95b4559c39f2ff87cdb3aaaa513",
|
||||
"size": 27971214
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x64.jar",
|
||||
"sha1": "f8da709c59ef61f531c91434ca0e3b4f39202981",
|
||||
"size": 49873846
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -4402,13 +4427,13 @@
|
||||
},
|
||||
"windows-x86": {
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:x86",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:x86",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-x86.jar",
|
||||
"sha1": "6fd0fd7da88ca6636f735e1e0f6feb7f62f59715",
|
||||
"size": 23311299
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-x86.jar",
|
||||
"sha1": "ac6afaa8baa7c17468267c09e77e1296ee92d5ed",
|
||||
"size": 41742113
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4791,13 +4816,13 @@
|
||||
"com.mojang:text2speech:1.12.4:natives": null,
|
||||
"com.mojang:text2speech:1.13.9:natives-windows": null,
|
||||
"mesa-loader": {
|
||||
"name": "org.glavo:mesa-loader-windows:25.0.3:arm64",
|
||||
"name": "org.glavo:mesa-loader-windows:26.0.4:arm64",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/25.0.3/mesa-loader-windows-25.0.3-arm64.jar",
|
||||
"sha1": "40ca5de3351bf48b7b9efa5d2598d5b6b4d1ab8e",
|
||||
"size": 24905270
|
||||
"path": "org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/glavo/mesa-loader-windows/26.0.4/mesa-loader-windows-26.0.4-arm64.jar",
|
||||
"sha1": "5c761a344700a07eaded51c5cf0cde36ee614706",
|
||||
"size": 43504189
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6325,6 +6350,83 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1.jar",
|
||||
"sha1": "5c02c88f0d879529014c78463b76f43fdecfd272",
|
||||
"size": 153051
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-shaderc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-shaderc:3.4.1:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-shaderc/3.4.1/lwjgl-shaderc-3.4.1-natives-freebsd.jar",
|
||||
"sha1": "872460f4b007ed3a13a2d311e90bda5370406fc8",
|
||||
"size": 2953522
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1.jar",
|
||||
"sha1": "188553592e97b5ab2dba54a1b403ee80b48dad4c",
|
||||
"size": 148285
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-spvc:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-spvc:3.4.1:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-spvc/3.4.1/lwjgl-spvc-3.4.1-natives-freebsd.jar",
|
||||
"sha1": "7a3d0e4a74f022b3196ecd33d9ebffafec7767f4",
|
||||
"size": 1094646
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1.jar",
|
||||
"sha1": "2c54ce10154feb1ede4d23c5947438cdbfdcc003",
|
||||
"size": 109209
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vma:3.4.1:natives-linux": {
|
||||
"name": "org.lwjgl:lwjgl-vma:3.4.1:natives-freebsd",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vma/3.4.1/lwjgl-vma-3.4.1-natives-freebsd.jar",
|
||||
"sha1": "85851480bef99188bf9a84b36cbe862cb4a52016",
|
||||
"size": 62862
|
||||
}
|
||||
}
|
||||
},
|
||||
"org.lwjgl:lwjgl-vulkan:3.4.1": {
|
||||
"name": "org.lwjgl:lwjgl-vulkan:3.4.1",
|
||||
"downloads": {
|
||||
"artifact": {
|
||||
"path": "org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"url": "https://repo1.maven.org/maven2/org/lwjgl/lwjgl-vulkan/3.4.1/lwjgl-vulkan-3.4.1.jar",
|
||||
"sha1": "d0601fdc38890755e5638048c14cc0f7b990964b",
|
||||
"size": 8356700
|
||||
}
|
||||
}
|
||||
},
|
||||
"net.java.jinput:jinput-platform:2.0.5:natives": null,
|
||||
"com.mojang:text2speech:1.10.3:natives": null,
|
||||
"com.mojang:text2speech:1.11.3:natives": null,
|
||||
|
||||
@@ -80,37 +80,37 @@ public class OAuth {
|
||||
}
|
||||
|
||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||
Session session = options.callback.startServer();
|
||||
try (Session session = options.callback.startServer()) {
|
||||
String codeVerifier = session.getCodeVerifier();
|
||||
String state = session.getState();
|
||||
String codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
|
||||
String codeVerifier = session.getCodeVerifier();
|
||||
String state = session.getState();
|
||||
String codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()),
|
||||
pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope),
|
||||
pair("prompt", "select_account"),
|
||||
pair("code_challenge", codeChallenge),
|
||||
pair("state", state),
|
||||
pair("code_challenge_method", "S256")
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
options.callback.openBrowser(GrantFlow.AUTHORIZATION_CODE, NetworkUtils.withQuery(authorizationURL,
|
||||
mapOf(pair("client_id", options.callback.getClientId()),
|
||||
pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope),
|
||||
pair("prompt", "select_account"),
|
||||
pair("code_challenge", codeChallenge),
|
||||
pair("state", state),
|
||||
pair("code_challenge_method", "S256")
|
||||
)));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()),
|
||||
pair("code", code),
|
||||
pair("grant_type", "authorization_code"),
|
||||
pair("code_verifier", codeVerifier),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.retry(5)
|
||||
.getJson(AuthorizationResponse.class);
|
||||
handleErrorResponse(response);
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
// Authorization Code -> Token
|
||||
AuthorizationResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()),
|
||||
pair("code", code),
|
||||
pair("grant_type", "authorization_code"),
|
||||
pair("code_verifier", codeVerifier),
|
||||
pair("redirect_uri", session.getRedirectURI()),
|
||||
pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.retry(5)
|
||||
.getJson(AuthorizationResponse.class);
|
||||
handleErrorResponse(response);
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
private Result authenticateDevice(Options options) throws IOException, InterruptedException, JsonParseException, AuthenticationException {
|
||||
@@ -234,7 +234,7 @@ public class OAuth {
|
||||
}
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
public interface Session extends AutoCloseable {
|
||||
String getState();
|
||||
|
||||
String getCodeVerifier();
|
||||
@@ -253,6 +253,9 @@ public class OAuth {
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
void close();
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
|
||||
+59
-37
@@ -37,40 +37,45 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Result> {
|
||||
|
||||
private static final String JAVA_LIST_URL = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
|
||||
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final Path target;
|
||||
private final Path tempDir;
|
||||
private final Task<MojangJavaRemoteFiles> javaDownloadsTask;
|
||||
private final List<Task<?>> dependencies = new ArrayList<>();
|
||||
|
||||
private volatile MojangJavaDownloads.JavaDownload download;
|
||||
|
||||
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, GameJavaVersion javaVersion, String platform) {
|
||||
public MojangJavaDownloadTask(DownloadProvider downloadProvider, Path target, Path tempDir, GameJavaVersion javaVersion, String platform) {
|
||||
this.target = target;
|
||||
this.tempDir = tempDir;
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(
|
||||
"https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json"))
|
||||
.thenComposeAsync(javaDownloadsJson -> {
|
||||
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
|
||||
this.javaDownloadsTask = new GetTask(downloadProvider.injectURLWithCandidates(JAVA_LIST_URL))
|
||||
.thenComposeAsync(javaDownloadsJson -> {
|
||||
MojangJavaDownloads allDownloads = JsonUtils.fromNonNullJson(javaDownloadsJson, MojangJavaDownloads.class);
|
||||
|
||||
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.getDownloads().get(platform);
|
||||
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
|
||||
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
|
||||
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
|
||||
for (MojangJavaDownloads.JavaDownload download : candidates) {
|
||||
if (JavaInfo.parseVersion(download.getVersion().getName()) >= javaVersion.majorVersion()) {
|
||||
this.download = download;
|
||||
return new GetTask(downloadProvider.injectURLWithCandidates(download.getManifest().getUrl()));
|
||||
}
|
||||
}
|
||||
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
|
||||
})
|
||||
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
|
||||
Map<String, List<MojangJavaDownloads.JavaDownload>> osDownloads = allDownloads.downloads().get(platform);
|
||||
if (osDownloads == null || !osDownloads.containsKey(javaVersion.component()))
|
||||
throw new UnsupportedPlatformException("Unsupported platform: " + platform);
|
||||
List<MojangJavaDownloads.JavaDownload> candidates = osDownloads.get(javaVersion.component());
|
||||
for (MojangJavaDownloads.JavaDownload download : candidates) {
|
||||
if (JavaInfo.parseVersion(download.version().name()) >= javaVersion.majorVersion()) {
|
||||
this.download = download;
|
||||
return new GetTask(downloadProvider.injectURLWithCandidates(download.manifest().getUrl()));
|
||||
}
|
||||
}
|
||||
throw new UnsupportedPlatformException("Candidates: " + JsonUtils.GSON.toJson(candidates));
|
||||
})
|
||||
.thenApplyAsync(javaDownloadJson -> JsonUtils.fromNonNullJson(javaDownloadJson, MojangJavaRemoteFiles.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,10 +86,8 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
for (Map.Entry<String, MojangJavaRemoteFiles.Remote> entry : javaDownloadsTask.getResult().getFiles().entrySet()) {
|
||||
Path dest = target.resolve(entry.getKey());
|
||||
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile) {
|
||||
MojangJavaRemoteFiles.RemoteFile file = ((MojangJavaRemoteFiles.RemoteFile) entry.getValue());
|
||||
|
||||
Path dest = tempDir.resolve(entry.getKey());
|
||||
if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteFile file) {
|
||||
// Use local file if it already exists
|
||||
try {
|
||||
BasicFileAttributes localFileAttributes = Files.readAttributes(dest, BasicFileAttributes.class);
|
||||
@@ -101,17 +104,34 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
|
||||
if (file.getDownloads().containsKey("lzma")) {
|
||||
DownloadInfo download = file.getDownloads().get("lzma");
|
||||
Path tempFile = target.resolve(entry.getKey() + ".lzma");
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile, new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
|
||||
DownloadInfo raw = file.getDownloads().get("raw");
|
||||
|
||||
String rawSha1;
|
||||
if (raw != null && raw.getSha1() != null) {
|
||||
rawSha1 = raw.getSha1();
|
||||
} else {
|
||||
rawSha1 = null;
|
||||
}
|
||||
|
||||
Path tempFile = tempDir.resolve(entry.getKey() + ".lzma");
|
||||
var task = new FileDownloadTask(downloadProvider.injectURLWithCandidates(download.getUrl()), tempFile,
|
||||
new FileDownloadTask.IntegrityCheck("SHA-1", download.getSha1()));
|
||||
task.setName(entry.getKey());
|
||||
dependencies.add(task.thenRunAsync(() -> {
|
||||
Path decompressed = target.resolve(entry.getKey() + ".tmp");
|
||||
try (LZMAInputStream input = new LZMAInputStream(Files.newInputStream(tempFile))) {
|
||||
Path decompressed = tempDir.resolve(entry.getKey() + ".tmp");
|
||||
var digest = MessageDigest.getInstance("SHA-1");
|
||||
try (var input = new DigestInputStream(new LZMAInputStream(Files.newInputStream(tempFile)), digest)) {
|
||||
Files.copy(input, decompressed, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed", e);
|
||||
}
|
||||
|
||||
String actualSha1 = HexFormat.of().formatHex(digest.digest());
|
||||
|
||||
if (rawSha1 != null && !actualSha1.equalsIgnoreCase(rawSha1)) {
|
||||
throw new ArtifactMalformedException("File " + entry.getKey() + " has incorrect SHA-1 hash: expected " + rawSha1 + ", got " + actualSha1);
|
||||
}
|
||||
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException e) {
|
||||
@@ -137,8 +157,7 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
}
|
||||
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteDirectory) {
|
||||
Files.createDirectories(dest);
|
||||
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink) {
|
||||
MojangJavaRemoteFiles.RemoteLink link = ((MojangJavaRemoteFiles.RemoteLink) entry.getValue());
|
||||
} else if (entry.getValue() instanceof MojangJavaRemoteFiles.RemoteLink link) {
|
||||
Files.deleteIfExists(dest);
|
||||
Files.createSymbolicLink(dest, Paths.get(link.getTarget()));
|
||||
}
|
||||
@@ -157,16 +176,19 @@ public final class MojangJavaDownloadTask extends Task<MojangJavaDownloadTask.Re
|
||||
|
||||
@Override
|
||||
public void postExecute() throws Exception {
|
||||
setResult(new Result(download, javaDownloadsTask.getResult()));
|
||||
}
|
||||
if (isDependenciesSucceeded()) {
|
||||
FileUtils.cleanDirectory(target);
|
||||
|
||||
public static final class Result {
|
||||
public final MojangJavaDownloads.JavaDownload download;
|
||||
public final MojangJavaRemoteFiles remoteFiles;
|
||||
|
||||
public Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
|
||||
this.download = download;
|
||||
this.remoteFiles = remoteFiles;
|
||||
if (Files.getFileStore(target).equals(Files.getFileStore(tempDir))) {
|
||||
Files.move(tempDir, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
} else {
|
||||
FileUtils.copyDirectory(tempDir, target);
|
||||
FileUtils.deleteDirectory(tempDir);
|
||||
}
|
||||
setResult(new Result(download, javaDownloadsTask.getResult()));
|
||||
}
|
||||
}
|
||||
|
||||
public record Result(MojangJavaDownloads.JavaDownload download, MojangJavaRemoteFiles remoteFiles) {
|
||||
}
|
||||
}
|
||||
+9
-83
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.download.java.mojang;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.JsonAdapter;
|
||||
import org.jackhuang.hmcl.game.DownloadInfo;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
@@ -29,21 +29,10 @@ import java.util.Map;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.listTypeOf;
|
||||
import static org.jackhuang.hmcl.util.gson.JsonUtils.mapTypeOf;
|
||||
|
||||
@JsonSerializable
|
||||
@JsonAdapter(MojangJavaDownloads.Adapter.class)
|
||||
public class MojangJavaDownloads {
|
||||
|
||||
private final Map<String, Map<String, List<JavaDownload>>> downloads;
|
||||
|
||||
public MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
|
||||
this.downloads = downloads;
|
||||
}
|
||||
|
||||
public Map<String, Map<String, List<JavaDownload>>> getDownloads() {
|
||||
return downloads;
|
||||
}
|
||||
|
||||
public record MojangJavaDownloads(Map<String, Map<String, List<JavaDownload>>> downloads) {
|
||||
public static class Adapter implements JsonSerializer<MojangJavaDownloads>, JsonDeserializer<MojangJavaDownloads> {
|
||||
|
||||
@Override
|
||||
public JsonElement serialize(MojangJavaDownloads src, Type typeOfSrc, JsonSerializationContext context) {
|
||||
return context.serialize(src.downloads);
|
||||
@@ -55,78 +44,15 @@ public class MojangJavaDownloads {
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class JavaDownload {
|
||||
private final Availability availability;
|
||||
private final DownloadInfo manifest;
|
||||
private final Version version;
|
||||
|
||||
public JavaDownload() {
|
||||
this(new Availability(), new DownloadInfo(), new Version());
|
||||
}
|
||||
|
||||
public JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
|
||||
this.availability = availability;
|
||||
this.manifest = manifest;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public Availability getAvailability() {
|
||||
return availability;
|
||||
}
|
||||
|
||||
public DownloadInfo getManifest() {
|
||||
return manifest;
|
||||
}
|
||||
|
||||
public Version getVersion() {
|
||||
return version;
|
||||
}
|
||||
@JsonSerializable
|
||||
public record JavaDownload(Availability availability, DownloadInfo manifest, Version version) {
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class Availability {
|
||||
private final int group;
|
||||
private final int progress;
|
||||
|
||||
public Availability() {
|
||||
this(0, 0);
|
||||
}
|
||||
|
||||
public Availability(int group, int progress) {
|
||||
this.group = group;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
public int getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
@JsonSerializable
|
||||
public record Availability(int group, int progress) {
|
||||
}
|
||||
|
||||
@Immutable
|
||||
public static class Version {
|
||||
private final String name;
|
||||
private final String released;
|
||||
|
||||
public Version() {
|
||||
this("", "");
|
||||
}
|
||||
|
||||
public Version(String name, String released) {
|
||||
this.name = name;
|
||||
this.released = released;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getReleased() {
|
||||
return released;
|
||||
}
|
||||
@JsonSerializable
|
||||
public record Version(String name, String released) {
|
||||
}
|
||||
}
|
||||
+28
-12
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonSerializable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -75,16 +76,38 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
String mcVersion;
|
||||
|
||||
try {
|
||||
int si1 = version.indexOf('.'), si2 = version.indexOf('.', version.indexOf('.') + 1);
|
||||
int si1 = version.indexOf('.');
|
||||
int si2 = version.indexOf('.', si1 + 1);
|
||||
if (si1 < 0 || si2 < 0) {
|
||||
LOG.warning("Unsupported NeoForge version: " + version);
|
||||
continue;
|
||||
}
|
||||
|
||||
int majorVersion = Integer.parseInt(version.substring(0, si1));
|
||||
if (majorVersion == 0) { // Snapshot version.
|
||||
mcVersion = version.substring(si1 + 1, si2);
|
||||
} else {
|
||||
String ver = version.substring(0, Integer.parseInt(version.substring(si1 + 1, si2)) == 0 ? si1 : si2);
|
||||
if (majorVersion >= 26) {
|
||||
int si3 = version.indexOf('.', si2 + 1);
|
||||
|
||||
if (si3 < 0) {
|
||||
LOG.warning("Unsupported NeoForge version: " + version);
|
||||
continue;
|
||||
}
|
||||
|
||||
String ver = Integer.parseInt(version.substring(si2 + 1, si3)) == 0
|
||||
? version.substring(0, si2)
|
||||
: version.substring(0, si3);
|
||||
|
||||
int separator = version.indexOf('+');
|
||||
mcVersion = separator < 0 ? ver : ver + "-" + version.substring(separator + 1);
|
||||
if (separator < 0)
|
||||
mcVersion = ver;
|
||||
else
|
||||
mcVersion = ver + "-" + version.substring(separator + 1);
|
||||
} else {
|
||||
String ver = Integer.parseInt(version.substring(si1 + 1, si2)) == 0
|
||||
? version.substring(0, si1)
|
||||
: version.substring(0, si2);
|
||||
mcVersion = "1." + ver;
|
||||
}
|
||||
}
|
||||
@@ -106,14 +129,7 @@ public final class NeoForgeOfficialVersionList extends VersionList<NeoForgeRemot
|
||||
});
|
||||
}
|
||||
|
||||
private static final class OfficialAPIResult {
|
||||
private final boolean isSnapshot;
|
||||
|
||||
private final List<String> versions;
|
||||
|
||||
public OfficialAPIResult(boolean isSnapshot, List<String> versions) {
|
||||
this.isSnapshot = isSnapshot;
|
||||
this.versions = versions;
|
||||
}
|
||||
@JsonSerializable
|
||||
private record OfficialAPIResult(boolean isSnapshot, List<String> versions) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/// The Graphics API.
|
||||
public enum GraphicsAPI {
|
||||
DEFAULT,
|
||||
OPENGL,
|
||||
VULKAN;
|
||||
|
||||
private final String minecraftArg = name().toLowerCase(Locale.ROOT);
|
||||
|
||||
public String getMinecraftArg() {
|
||||
return minecraftArg;
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ public class LaunchOptions implements Serializable {
|
||||
private NativesDirectoryType nativesDirType;
|
||||
private String nativesDir;
|
||||
private ProcessPriority processPriority = ProcessPriority.NORMAL;
|
||||
private GraphicsAPI graphicsBackend = GraphicsAPI.DEFAULT;
|
||||
private Renderer renderer = Renderer.DEFAULT;
|
||||
private boolean useNativeGLFW;
|
||||
private boolean useNativeOpenAL;
|
||||
@@ -244,7 +245,11 @@ public class LaunchOptions implements Serializable {
|
||||
return processPriority;
|
||||
}
|
||||
|
||||
public Renderer getRenderer() {
|
||||
public @NotNull GraphicsAPI getGraphicsBackend() {
|
||||
return graphicsBackend;
|
||||
}
|
||||
|
||||
public @NotNull Renderer getRenderer() {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -435,8 +440,13 @@ public class LaunchOptions implements Serializable {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenderer(@NotNull Renderer renderer) {
|
||||
options.renderer = renderer;
|
||||
public Builder setGraphicsBackend(GraphicsAPI backend) {
|
||||
options.graphicsBackend = Objects.requireNonNullElse(backend, GraphicsAPI.DEFAULT);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setRenderer(Renderer renderer) {
|
||||
options.renderer = Objects.requireNonNullElse(renderer, Renderer.DEFAULT);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,9 +17,529 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
public enum Renderer {
|
||||
DEFAULT,
|
||||
ZINK,
|
||||
LLVMPIPE,
|
||||
D3D12
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.GraphicsCard;
|
||||
import org.jackhuang.hmcl.util.platform.hardware.HardwareVendor;
|
||||
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public sealed interface Renderer permits Renderer.Default, Renderer.Driver, Renderer.Unknown {
|
||||
|
||||
/// Default renderer.
|
||||
Default DEFAULT = new Default();
|
||||
|
||||
/// Parse a renderer from a string.
|
||||
static Renderer of(String name) {
|
||||
if ("DEFAULT".equalsIgnoreCase(name) || name.isBlank())
|
||||
return DEFAULT;
|
||||
|
||||
String upper = name.toUpperCase(Locale.ROOT).trim();
|
||||
|
||||
try {
|
||||
return OpenGL.valueOf(upper);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
return Vulkan.valueOf(upper);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
|
||||
return new Unknown(name);
|
||||
}
|
||||
|
||||
/// Get all supported renderers for a given graphics API.
|
||||
static @Unmodifiable List<Renderer> getSupported(GraphicsAPI api) {
|
||||
return switch (api) {
|
||||
case DEFAULT -> List.of(DEFAULT);
|
||||
case VULKAN -> Vulkan.Holder.SUPPORTED;
|
||||
case OPENGL -> OpenGL.SUPPORTED;
|
||||
};
|
||||
}
|
||||
|
||||
String name();
|
||||
|
||||
final class Default implements Renderer {
|
||||
private Default() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return "DEFAULT";
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface Driver extends Renderer permits Vulkan, OpenGL {
|
||||
|
||||
/// The graphics API that this driver belongs to.
|
||||
GraphicsAPI api();
|
||||
|
||||
/// If this driver can be loaded via [mesa-loader-windows](https://github.com/HMCL-dev/mesa-loader-windows),
|
||||
/// return the driver name, otherwise return `null`.
|
||||
@Contract(pure = true)
|
||||
default @Nullable String mesaDriverName() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
enum Vulkan implements Driver {
|
||||
/// Mesa Lavapipe driver.
|
||||
///
|
||||
/// It is a software rasterizer.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
|
||||
LAVAPIPE("lvp") {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "lavapipe";
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa Dozen driver.
|
||||
///
|
||||
/// It is a Vulkan driver based on DirectX 12.
|
||||
///
|
||||
/// ## Note
|
||||
/// Currently, Dozen does not support the VK_KHR_push_descriptor feature, so it cannot launch Minecraft 26.2
|
||||
/// Using Dozen can run Minecraft 1.21.11 + VulkanMod, but it will cause the game to crash after playing for a while
|
||||
DOZEN("dzn") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "dzn";
|
||||
}
|
||||
},
|
||||
|
||||
/// NVIDIA Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for NVIDIA GPUs.
|
||||
///
|
||||
/// @see <a href="https://developer.nvidia.com/vulkan">Vulkan Open Standard Modern GPU API | NVIDIA Developer</a>
|
||||
NVIDIA_VULKAN("nvidia") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa NVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for NVIDIA GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/nvk.html">NVK - The Mesa 3D Graphics Library</a>
|
||||
NVIDIA_NVK("nouveau") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.NVIDIA);
|
||||
}
|
||||
},
|
||||
|
||||
/// AMD Open Source Driver for Vulkan
|
||||
///
|
||||
/// It is a Vulkan driver for AMD GPUs.
|
||||
///
|
||||
/// @see <a href="https://github.com/GPUOpen-Drivers/AMDVLK">GPUOpen-Drivers/AMDVLK - GitHub</a>
|
||||
AMDVLK("amd") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.AMD);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa RADV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for AMD GCN/RDNA GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/radv.html">RADV - The Mesa 3D Graphics Library</a>
|
||||
AMD_RADV("radeon") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return Vulkan.hasCard(cards, HardwareVendor.AMD);
|
||||
}
|
||||
},
|
||||
|
||||
/// Intel Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel GPUs.
|
||||
INTEL_VULKAN("ig") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa ANV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/anv.html">ANV - The Mesa 3D Graphics Library</a>
|
||||
INTEL_ANV("intel") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS && Vulkan.hasCard(cards, HardwareVendor.INTEL);
|
||||
}
|
||||
},
|
||||
|
||||
/// Intel HASVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Intel Gen7 (Ivy Bridge / Haswell) and Gen8 (Broadwell) graphics.
|
||||
///
|
||||
/// @see <a href="https://gitlab.freedesktop.org/mesa/mesa/-/merge_requests/18208">intel: split vulkan driver between gfx7/8 and above</a>
|
||||
INTEL_HASVK("intel_hasvk") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS
|
||||
&& cards != null
|
||||
&& cards.stream().anyMatch(card -> card.getVendor() == HardwareVendor.INTEL && card.getName().startsWith("Intel HD Graphics "));
|
||||
}
|
||||
},
|
||||
|
||||
/// Qualcomm Vulkan driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
|
||||
QUALCOMM("qc") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.equals(Platform.WINDOWS_ARM64);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa Turnip driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Qualcomm Adreno GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/freedreno.html">Freedreno - The Mesa 3D Graphics Library</a>
|
||||
TURNIP("freedreno") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() != OperatingSystem.WINDOWS && platform.arch().isArm();
|
||||
}
|
||||
},
|
||||
|
||||
/// MoltenVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for macOS, iOS, tvOS, and visionOS.
|
||||
///
|
||||
/// @see <a href="https://github.com/KhronosGroup/MoltenVK">KhronosGroup/MoltenVK - GitHub</a>
|
||||
MOLTENVK("MoltenVK") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.MACOS;
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa KosmicKrisp driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Apple Silicon hardware.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/kosmickrisp.html">KosmicKrisp - The Mesa 3D Graphics Library</a>
|
||||
KOSMICKRISP("kosmickrisp_mesa") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.MACOS && platform.arch() == Architecture.ARM64;
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa PowerVR driver.
|
||||
///
|
||||
/// It is a Vulkan driver for Imagination Technologies PowerVR GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/powervr.html">PowerVR - The Mesa 3D Graphics Library</a>
|
||||
POWERVR("powervr") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && Vulkan.hasCard(cards, HardwareVendor.IMG);
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa PanVK driver.
|
||||
///
|
||||
/// It is a Vulkan driver for ARM Mali GPUs.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/panfrost.html">Panfrost - The Mesa 3D Graphics Library</a>
|
||||
PANVK("panfrost") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm();
|
||||
}
|
||||
},
|
||||
|
||||
/// Mesa V3DV driver.
|
||||
///
|
||||
/// It is a Vulkan driver for the Raspberry Pi 4 and Raspberry Pi 5.
|
||||
///
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/v3dv.html">V3DV - The Mesa 3D Graphics Library</a>
|
||||
V3DV("broadcom") {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.LINUX && platform.arch().isArm()
|
||||
&& Vulkan.hasCard(cards, HardwareVendor.BROADCOM);
|
||||
}
|
||||
};
|
||||
|
||||
private static final class Holder {
|
||||
static final List<Renderer> SUPPORTED;
|
||||
static final Map<Vulkan, Path> DRIVER_TO_ICD_FILE;
|
||||
|
||||
static {
|
||||
var driverToIcdFile = new EnumMap<Vulkan, Path>(Vulkan.class);
|
||||
var supported = new LinkedHashSet<Renderer>();
|
||||
|
||||
|
||||
// Helper
|
||||
String archName = switch (Architecture.SYSTEM_ARCH) {
|
||||
case X86 -> "i686";
|
||||
case X86_64 -> "x86_64";
|
||||
case ARM64 -> "aarch64";
|
||||
default -> Architecture.SYSTEM_ARCH.getCheckedName();
|
||||
};
|
||||
|
||||
var icdFileNamePattern = Pattern.compile("(?<name>[a-zA-Z0-9_-]+)_icd(?:\\." + Pattern.quote(archName) + ")?\\.json");
|
||||
Map<String, Vulkan> icdNameToDriver = Stream.of(values()).collect(Collectors.toMap(Vulkan::icdName, Function.identity()));
|
||||
|
||||
supported.add(DEFAULT);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
supported.addAll(List.of(LAVAPIPE, DOZEN));
|
||||
|
||||
List<GraphicsCard> graphicsCards = SystemInfo.getGraphicsCards();
|
||||
if (graphicsCards != null) {
|
||||
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
|
||||
for (GraphicsCard card : graphicsCards) {
|
||||
if (!card.getVulkanDriverFiles().isEmpty()) {
|
||||
for (Path icdFile : card.getVulkanDriverFiles()) {
|
||||
String fileName = FileUtils.getName(icdFile);
|
||||
if (!fileName.endsWith(".json"))
|
||||
continue;
|
||||
|
||||
Vulkan driver;
|
||||
|
||||
Matcher matcher = icdFileNamePattern.matcher(fileName);
|
||||
if (matcher.matches()) {
|
||||
String icdName = matcher.group("name");
|
||||
driver = icdNameToDriver.get(icdName);
|
||||
} else {
|
||||
switch (fileName.substring(0, fileName.length() - ".json".length())) {
|
||||
case "igvk64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = INTEL_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "igvk32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = INTEL_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "nv-vk64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = NVIDIA_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "nv-vk32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = NVIDIA_VULKAN;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "amd-vulkan64" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_64)
|
||||
driver = AMDVLK;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "amd-vulkan32" -> {
|
||||
if (Architecture.SYSTEM_ARCH.getBits() == Bits.BIT_32)
|
||||
driver = AMDVLK;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
case "qcvk_icd_arm64x" -> {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64)
|
||||
driver = QUALCOMM;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
default -> {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
driverToIcdFile.putIfAbsent(driver, icdFile);
|
||||
foundSupported.add(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
supported.addAll(foundSupported);
|
||||
}
|
||||
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
// LWJGL integrates MoltenVK, so it is always available
|
||||
supported.add(MOLTENVK);
|
||||
|
||||
// We need libvulkan.1.dylib to load custom Vulkan drivers
|
||||
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
|
||||
Path kosmickrispIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/kosmickrisp_mesa_icd." + archName + ".json");
|
||||
if (Files.isRegularFile(kosmickrispIcd)) {
|
||||
driverToIcdFile.put(KOSMICKRISP, kosmickrispIcd);
|
||||
supported.add(KOSMICKRISP);
|
||||
}
|
||||
|
||||
Path lvpIcd = HomebrewUtils.HOMEBREW_PREFIX.resolve("share/vulkan/icd.d/lvp_icd." + archName + ".json");
|
||||
if (Files.isRegularFile(lvpIcd)) {
|
||||
driverToIcdFile.put(LAVAPIPE, lvpIcd);
|
||||
supported.add(LAVAPIPE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
List<Path> icdDirs = switch (OperatingSystem.CURRENT_OS) {
|
||||
case LINUX -> List.of(
|
||||
Path.of("/usr/share/vulkan/icd.d"),
|
||||
Path.of("/etc/vulkan/icd.d")
|
||||
);
|
||||
case FREEBSD -> List.of(Path.of("/usr/local/share/vulkan/icd.d"));
|
||||
default -> List.of();
|
||||
};
|
||||
|
||||
EnumSet<Vulkan> foundSupported = EnumSet.noneOf(Vulkan.class);
|
||||
for (Path icdDir : icdDirs) {
|
||||
if (!Files.isDirectory(icdDir))
|
||||
continue;
|
||||
try (Stream<Path> stream = Files.list(icdDir)) {
|
||||
for (Path icdFile : Lang.toIterable(stream)) {
|
||||
String fileName = icdFile.getFileName().toString();
|
||||
|
||||
Matcher matcher = icdFileNamePattern.matcher(fileName);
|
||||
if (matcher.matches()) {
|
||||
String icdName = matcher.group("name");
|
||||
|
||||
Vulkan driver = icdNameToDriver.get(icdName);
|
||||
if (driver != null) {
|
||||
driverToIcdFile.put(driver, icdFile);
|
||||
|
||||
if (driver.isSupported(Platform.CURRENT_PLATFORM, SystemInfo.getGraphicsCards())) {
|
||||
foundSupported.add(driver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to read Vulkan ICD files in " + icdDir, e);
|
||||
}
|
||||
}
|
||||
|
||||
supported.addAll(foundSupported);
|
||||
}
|
||||
|
||||
SUPPORTED = List.copyOf(supported);
|
||||
DRIVER_TO_ICD_FILE = Collections.unmodifiableMap(driverToIcdFile);
|
||||
}
|
||||
}
|
||||
|
||||
private final String icdName;
|
||||
|
||||
Vulkan(String icdName) {
|
||||
this.icdName = icdName;
|
||||
}
|
||||
|
||||
private static boolean hasCard(@Nullable List<GraphicsCard> cards, HardwareVendor vendor) {
|
||||
return cards != null && cards.stream().anyMatch(card -> card.getVendor() == vendor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphicsAPI api() {
|
||||
return GraphicsAPI.VULKAN;
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public String icdName() {
|
||||
return icdName;
|
||||
}
|
||||
|
||||
/// Get the path to the ICD file for this driver.
|
||||
///
|
||||
/// If the ICD file does not exist, return `null`.
|
||||
@Contract(pure = true)
|
||||
public @Nullable Path icdFile() {
|
||||
return Holder.DRIVER_TO_ICD_FILE.get(this);
|
||||
}
|
||||
}
|
||||
|
||||
enum OpenGL implements Driver {
|
||||
/// @see <a href="https://docs.mesa3d.org/drivers/llvmpipe.html">LLVMpipe - The Mesa 3D Graphics Library</a>
|
||||
LLVMPIPE {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "llvmpipe";
|
||||
}
|
||||
},
|
||||
ZINK {
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "zink";
|
||||
}
|
||||
},
|
||||
D3D12 {
|
||||
@Override
|
||||
public boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return platform.os() == OperatingSystem.WINDOWS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String mesaDriverName() {
|
||||
return "d3d12";
|
||||
}
|
||||
};
|
||||
|
||||
static final List<Renderer> SUPPORTED =
|
||||
Stream.concat(
|
||||
Stream.of(DEFAULT),
|
||||
Stream.of(OpenGL.values()).filter(it -> it.isSupported(
|
||||
Platform.CURRENT_PLATFORM,
|
||||
null // We don't need to pass graphics cards for OpenGL yet
|
||||
))
|
||||
).toList();
|
||||
|
||||
@Override
|
||||
public GraphicsAPI api() {
|
||||
return GraphicsAPI.OPENGL;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unknown renderer.
|
||||
record Unknown(String name) implements Renderer {
|
||||
}
|
||||
|
||||
/// Whether this renderer is supported on the given platform and graphics cards.
|
||||
///
|
||||
/// If `cards` is `null`, it means that the graphics cards are unknown.
|
||||
default boolean isSupported(Platform platform, @Nullable List<GraphicsCard> cards) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,14 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -85,12 +85,6 @@ public final class JavaInfo {
|
||||
return new JavaInfo(Platform.getPlatform(os, arch), javaVersion, vendor);
|
||||
}
|
||||
|
||||
public static JavaInfo fromReleaseFile(Path releaseFile) throws IOException {
|
||||
try (BufferedReader reader = Files.newBufferedReader(releaseFile)) {
|
||||
return fromReleaseFile(reader);
|
||||
}
|
||||
}
|
||||
|
||||
public static <F, E extends ArchiveEntry> JavaInfo fromArchive(ArchiveFileTree<F, E> tree) throws IOException {
|
||||
if (tree.getRoot().getSubDirs().size() != 1 || !tree.getRoot().getFiles().isEmpty())
|
||||
throw new IOException();
|
||||
@@ -116,24 +110,40 @@ public final class JavaInfo {
|
||||
if (vendor == null)
|
||||
return null;
|
||||
|
||||
switch (vendor) {
|
||||
case "N/A":
|
||||
return null;
|
||||
case "Oracle Corporation":
|
||||
return "Oracle";
|
||||
case "Azul Systems, Inc.":
|
||||
return "Azul";
|
||||
case "IBM Corporation":
|
||||
case "International Business Machines Corporation":
|
||||
case "Eclipse OpenJ9":
|
||||
return "IBM";
|
||||
case "Eclipse Adoptium":
|
||||
return "Adoptium";
|
||||
case "Amazon.com Inc.":
|
||||
return "Amazon";
|
||||
default:
|
||||
return vendor;
|
||||
return switch (vendor) {
|
||||
case "N/A" -> null;
|
||||
case "Oracle Corporation" -> "Oracle";
|
||||
case "Azul Systems, Inc." -> "Azul";
|
||||
case "IBM Corporation", "International Business Machines Corporation", "Eclipse OpenJ9" -> "IBM";
|
||||
case "Eclipse Adoptium" -> "Adoptium";
|
||||
case "Amazon.com Inc." -> "Amazon";
|
||||
default -> vendor;
|
||||
};
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private final Platform platform;
|
||||
private final String version;
|
||||
private @Nullable String vendor;
|
||||
|
||||
public Builder(Platform platform, String version) {
|
||||
this.platform = platform;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Contract("_ -> this")
|
||||
public Builder setVendor(@Nullable String vendor) {
|
||||
this.vendor = vendor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavaInfo build() {
|
||||
return new JavaInfo(platform, version, vendor);
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder newBuilder(@NotNull Platform platform, @NotNull String version) {
|
||||
return new Builder(platform, version);
|
||||
}
|
||||
|
||||
public static final JavaInfo CURRENT_ENVIRONMENT = new JavaInfo(Platform.CURRENT_PLATFORM, System.getProperty("java.version"), System.getProperty("java.vendor"));
|
||||
|
||||
@@ -34,7 +34,7 @@ public interface JavaRepository {
|
||||
|
||||
Path getManifestFile(Platform platform, String name);
|
||||
|
||||
Collection<JavaRuntime> getAllJava(Platform platform);
|
||||
Collection<Path> getAllJava(Platform platform);
|
||||
|
||||
Task<JavaRuntime> getDownloadJavaTask(DownloadProvider downloadProvider, Platform platform, GameJavaVersion gameJavaVersion);
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.Unzipper;
|
||||
import org.jackhuang.hmcl.util.platform.*;
|
||||
import org.jackhuang.hmcl.util.platform.macos.HomebrewUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.Charset;
|
||||
@@ -103,7 +105,7 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
// Executable
|
||||
if (StringUtils.isNotBlank(options.getWrapper()))
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars()));
|
||||
res.addAllWithoutParsing(StringUtils.tokenize(options.getWrapper(), getEnvVars(nativeFolder)));
|
||||
|
||||
res.add(options.getJava().getBinary().toString());
|
||||
|
||||
@@ -264,6 +266,21 @@ public class DefaultLauncher extends Launcher {
|
||||
res.addDefault("-Dfml.ignorePatchDiscrepancies=", "true");
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& options.getRenderer() instanceof Renderer.Driver renderer
|
||||
&& renderer.mesaDriverName() != null) {
|
||||
res.addDefault("-Dorg.glavo.mesa.loader.nativeDir=", FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader")));
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
&& options.getRenderer() instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver.icdFile() != null) {
|
||||
if (Files.isRegularFile(HomebrewUtils.LIB_VULKAN)) {
|
||||
res.addDefault("-Dorg.lwjgl.vulkan.libname=", FileUtils.getAbsolutePath(HomebrewUtils.LIB_VULKAN));
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> classpath = repository.getClasspath(version);
|
||||
|
||||
if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) {
|
||||
@@ -296,7 +313,36 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
configuration.put("${natives_directory}", nativeFolderPath);
|
||||
|
||||
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
|
||||
Path javaNativeFolder = FileUtils.toAbsolute(nativeFolder);
|
||||
@Nullable List<Argument> jvmArguments = version.getArguments().map(Arguments::getJvm).orElse(null);
|
||||
|
||||
if (jvmArguments != null) {
|
||||
for (Argument jvmArgument : jvmArguments) {
|
||||
if (jvmArgument instanceof StringArgument stringArgument
|
||||
&& stringArgument.getArgument().startsWith("-Djava.library.path=")) {
|
||||
|
||||
// We conservatively handle parameters like "-Djava.library.path=${natives_directory}/java"
|
||||
// to avoid extracting native libraries to unexpected locations.
|
||||
|
||||
String prefix = "-Djava.library.path=${natives_directory}/";
|
||||
if (stringArgument.getArgument().startsWith(prefix)) {
|
||||
try {
|
||||
String subDir = stringArgument.getArgument().substring(prefix.length());
|
||||
Path actualNativeFolder = FileUtils.toAbsolute(javaNativeFolder.resolve(subDir));
|
||||
|
||||
if (actualNativeFolder.startsWith(javaNativeFolder)) {
|
||||
javaNativeFolder = actualNativeFolder;
|
||||
}
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.addAll(Arguments.parseArguments(Objects.requireNonNullElseGet(jvmArguments, this::getDefaultJVMArguments), configuration));
|
||||
Arguments argumentsFromAuthInfo = authInfo.getLaunchArguments(options);
|
||||
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getJvm() != null && !argumentsFromAuthInfo.getJvm().isEmpty())
|
||||
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getJvm(), configuration));
|
||||
@@ -305,6 +351,10 @@ public class DefaultLauncher extends Launcher {
|
||||
res.add("-javaagent:" + javaAgent);
|
||||
}
|
||||
|
||||
if (version.getMainClass() == null) {
|
||||
throw new IllegalStateException("Main class is null for instance " + version.getId());
|
||||
}
|
||||
|
||||
res.add(version.getMainClass());
|
||||
|
||||
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(ArrayList::new), configuration));
|
||||
@@ -361,10 +411,16 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
if (options.getGraphicsBackend() != GraphicsAPI.DEFAULT
|
||||
&& gameVersion.isPresent() && GameVersionNumber.compare(gameVersion.get(), "26.2-snapshot-2") >= 0) {
|
||||
res.add("--graphicsBackend");
|
||||
res.add(options.getGraphicsBackend().getMinecraftArg());
|
||||
}
|
||||
|
||||
res.addAllWithoutParsing(Arguments.parseStringArguments(options.getGameArguments(), configuration));
|
||||
|
||||
res.removeIf(it -> getForbiddens().containsKey(it) && getForbiddens().get(it).get());
|
||||
return new Command(res, tempNativeFolder, encoding);
|
||||
return new Command(res, tempNativeFolder, javaNativeFolder, encoding);
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getFeatures() {
|
||||
@@ -401,6 +457,8 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
|
||||
public void decompressNatives(Path destination) throws NotDecompressingNativesException {
|
||||
LOG.info("Decompress native libraries to " + destination);
|
||||
|
||||
try {
|
||||
FileUtils.cleanDirectoryQuietly(destination);
|
||||
for (Library library : version.getLibraries())
|
||||
@@ -519,7 +577,7 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
|
||||
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
decompressNatives(nativeFolder);
|
||||
decompressNatives(command.javaNativeFolder);
|
||||
}
|
||||
|
||||
if (isUsingLog4j())
|
||||
@@ -528,8 +586,8 @@ public class DefaultLauncher extends Launcher {
|
||||
Path runDirectory = repository.getRunDirectory(version.getId());
|
||||
|
||||
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars())).directory(runDirectory.toFile());
|
||||
builder.environment().putAll(getEnvVars());
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars(nativeFolder))).directory(runDirectory.toFile());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
SystemUtils.callExternalProcess(builder);
|
||||
}
|
||||
|
||||
@@ -542,7 +600,7 @@ public class DefaultLauncher extends Launcher {
|
||||
Path appdata = options.getGameDir().toAbsolutePath().getParent();
|
||||
if (appdata != null) builder.environment().put("APPDATA", appdata.toString());
|
||||
|
||||
builder.environment().putAll(getEnvVars());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
process = builder.start();
|
||||
} catch (IOException e) {
|
||||
throw new ProcessCreationException(e);
|
||||
@@ -550,12 +608,13 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
|
||||
if (listener != null)
|
||||
startMonitors(p, listener, command.encoding, daemon);
|
||||
startMonitors(p, nativeFolder, listener, command.encoding, daemon);
|
||||
return p;
|
||||
}
|
||||
|
||||
private Map<String, String> getEnvVars() {
|
||||
private Map<String, String> getEnvVars(Path nativeFolder) {
|
||||
String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId());
|
||||
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("INST_NAME", versionName);
|
||||
env.put("INST_ID", versionName);
|
||||
@@ -563,18 +622,32 @@ public class DefaultLauncher extends Launcher {
|
||||
env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId())));
|
||||
env.put("INST_JAVA", options.getJava().getBinary().toString());
|
||||
|
||||
Renderer renderer = options.getRenderer();
|
||||
if (renderer != Renderer.DEFAULT) {
|
||||
if (options.getRenderer() instanceof Renderer.Driver driver) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (renderer != Renderer.LLVMPIPE)
|
||||
env.put("GALLIUM_DRIVER", renderer.name().toLowerCase(Locale.ROOT));
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
switch (renderer) {
|
||||
case LLVMPIPE:
|
||||
if (driver.mesaDriverName() != null) {
|
||||
if (driver instanceof Renderer.OpenGL && driver != Renderer.OpenGL.LLVMPIPE)
|
||||
env.put("GALLIUM_DRIVER", driver.mesaDriverName());
|
||||
else if (driver instanceof Renderer.Vulkan vulkanDriver) {
|
||||
String icdFile = FileUtils.getAbsolutePath(nativeFolder.resolve("mesa-loader/" + vulkanDriver.icdName() + "_icd.json"));
|
||||
|
||||
env.put("VK_ICD_FILENAMES", icdFile);
|
||||
env.put("VK_DRIVER_FILES", icdFile);
|
||||
}
|
||||
} else if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver.icdFile() != null
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
String icdFile = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
|
||||
env.put("VK_ICD_FILENAMES", icdFile);
|
||||
env.put("VK_DRIVER_FILES", icdFile);
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS.isLinuxOrBSD()) {
|
||||
if (driver instanceof Renderer.OpenGL oglDriver) {
|
||||
if (oglDriver == Renderer.OpenGL.LLVMPIPE) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
env.put("LIBGL_ALWAYS_SOFTWARE", "1");
|
||||
break;
|
||||
case ZINK:
|
||||
} else if (oglDriver == Renderer.OpenGL.ZINK) {
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "mesa");
|
||||
env.put("MESA_LOADER_DRIVER_OVERRIDE", "zink");
|
||||
/*
|
||||
* The amdgpu DDX is missing support for modifiers, causing Zink to fail.
|
||||
@@ -583,7 +656,23 @@ public class DefaultLauncher extends Launcher {
|
||||
* Link: https://gitlab.freedesktop.org/mesa/mesa/-/issues/10093
|
||||
*/
|
||||
env.put("LIBGL_KOPPER_DRI2", "1");
|
||||
break;
|
||||
}
|
||||
} else if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
if (vulkanDriver.icdFile() != null) {
|
||||
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
env.put("VK_ICD_FILENAMES", absolutePath);
|
||||
env.put("VK_DRIVER_FILES", absolutePath);
|
||||
}
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|
||||
&& options.getJava().getArchitecture() == Architecture.SYSTEM_ARCH) {
|
||||
if (driver instanceof Renderer.Vulkan vulkanDriver
|
||||
&& vulkanDriver != Renderer.Vulkan.MOLTENVK
|
||||
&& vulkanDriver.icdFile() != null) {
|
||||
String absolutePath = FileUtils.getAbsolutePath(vulkanDriver.icdFile());
|
||||
env.put("VK_ICD_FILENAMES", absolutePath);
|
||||
env.put("VK_DRIVER_FILES", absolutePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,13 +718,6 @@ public class DefaultLauncher extends Launcher {
|
||||
nativeFolder = Path.of(options.getNativesDir());
|
||||
}
|
||||
|
||||
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
decompressNatives(nativeFolder);
|
||||
}
|
||||
|
||||
if (isUsingLog4j())
|
||||
extractLog4jConfigurationFile();
|
||||
|
||||
String scriptExtension = FileUtils.getExtension(scriptFile);
|
||||
boolean usePowerShell = "ps1".equals(scriptExtension);
|
||||
|
||||
@@ -648,7 +730,7 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
final Command commandLine = generateCommandLine(nativeFolder);
|
||||
final String command = usePowerShell ? null : commandLine.commandLine.toString();
|
||||
Map<String, String> envVars = getEnvVars();
|
||||
Map<String, String> envVars = getEnvVars(nativeFolder);
|
||||
|
||||
if (isWindows && !usePowerShell) {
|
||||
// https://stackoverflow.com/a/28452546
|
||||
@@ -658,6 +740,12 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
if (isUsingLog4j())
|
||||
extractLog4jConfigurationFile();
|
||||
|
||||
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER)
|
||||
decompressNatives(commandLine.javaNativeFolder);
|
||||
|
||||
Files.createDirectories(scriptFile.getParent());
|
||||
|
||||
try (OutputStream outputStream = Files.newOutputStream(scriptFile)) {
|
||||
@@ -785,7 +873,7 @@ public class DefaultLauncher extends Launcher {
|
||||
throw new ExecutionPolicyLimitException();
|
||||
}
|
||||
|
||||
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, Charset encoding, boolean isDaemon) {
|
||||
private void startMonitors(ManagedProcess managedProcess, Path nativeFolder, ProcessListener processListener, Charset encoding, boolean isDaemon) {
|
||||
processListener.setProcess(managedProcess);
|
||||
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
|
||||
processListener.onLog(it, false);
|
||||
@@ -802,8 +890,8 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
|
||||
try {
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars())).directory(options.getGameDir().toFile());
|
||||
builder.environment().putAll(getEnvVars());
|
||||
ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPostExitCommand(), getEnvVars(nativeFolder))).directory(options.getGameDir().toFile());
|
||||
builder.environment().putAll(getEnvVars(nativeFolder));
|
||||
SystemUtils.callExternalProcess(builder);
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("An Exception happened while running exit command.", e);
|
||||
@@ -812,15 +900,10 @@ public class DefaultLauncher extends Launcher {
|
||||
}), "exit-waiter", isDaemon));
|
||||
}
|
||||
|
||||
private static final class Command {
|
||||
final CommandBuilder commandLine;
|
||||
final Path tempNativeFolder;
|
||||
final Charset encoding;
|
||||
|
||||
Command(CommandBuilder commandBuilder, Path tempNativeFolder, Charset encoding) {
|
||||
this.commandLine = commandBuilder;
|
||||
this.tempNativeFolder = tempNativeFolder;
|
||||
this.encoding = encoding;
|
||||
}
|
||||
private record Command(
|
||||
CommandBuilder commandLine,
|
||||
@Nullable Path tempNativeFolder,
|
||||
Path javaNativeFolder,
|
||||
Charset encoding) {
|
||||
}
|
||||
}
|
||||
@@ -293,7 +293,7 @@ public final class ForgeNewModMetadata {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) throws IOException {
|
||||
private static ModLoaderType analyzeLoader(Toml toml, String modID, ModLoaderType loader) {
|
||||
List<HashMap<String, Object>> dependencies = null;
|
||||
try {
|
||||
dependencies = toml.getList("dependencies." + modID);
|
||||
@@ -332,11 +332,11 @@ public final class ForgeNewModMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
if (result == loader)
|
||||
if (result != null) {
|
||||
if (result != loader)
|
||||
LOG.warning("Loader mismatch for mod " + modID + ", found " + result + ", expecting " + loader);
|
||||
return result;
|
||||
else if (result != null)
|
||||
throw new IOException("Loader mismatch");
|
||||
else {
|
||||
} else {
|
||||
LOG.warning("Cannot determine the mod loader for mod " + modID + ", expected " + loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ public enum Architecture {
|
||||
return this == X86 || this == X86_64;
|
||||
}
|
||||
|
||||
public boolean isArm() {
|
||||
return this == ARM32 || this == ARM64;
|
||||
}
|
||||
|
||||
public static final Architecture CURRENT_ARCH;
|
||||
public static final Architecture SYSTEM_ARCH;
|
||||
|
||||
|
||||
+38
-16
@@ -18,18 +18,22 @@
|
||||
package org.jackhuang.hmcl.util.platform.hardware;
|
||||
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnknownNullability;
|
||||
import org.jetbrains.annotations.Unmodifiable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @author Glavo
|
||||
*/
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class GraphicsCard {
|
||||
|
||||
public static String cleanName(String name) {
|
||||
@UnknownNullability
|
||||
public static String cleanName(@UnknownNullability String name) {
|
||||
if (name == null)
|
||||
return null;
|
||||
|
||||
@@ -58,13 +62,15 @@ public final class GraphicsCard {
|
||||
private final @Nullable Type type;
|
||||
private final @Nullable String driver;
|
||||
private final @Nullable String driverVersion;
|
||||
private final @Unmodifiable List<Path> vulkanDriverFiles;
|
||||
|
||||
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion) {
|
||||
private GraphicsCard(String name, @Nullable HardwareVendor vendor, @Nullable Type type, @Nullable String driver, @Nullable String driverVersion, List<Path> vulkanDriverFiles) {
|
||||
this.name = Objects.requireNonNull(name);
|
||||
this.vendor = vendor;
|
||||
this.type = type;
|
||||
this.driver = driver;
|
||||
this.driverVersion = driverVersion;
|
||||
this.vulkanDriverFiles = vulkanDriverFiles;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -79,6 +85,10 @@ public final class GraphicsCard {
|
||||
return driverVersion;
|
||||
}
|
||||
|
||||
public List<Path> getVulkanDriverFiles() {
|
||||
return vulkanDriverFiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder(name);
|
||||
@@ -96,11 +106,12 @@ public final class GraphicsCard {
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String name;
|
||||
private HardwareVendor vendor;
|
||||
private Type type;
|
||||
private String driver;
|
||||
private String driverVersion;
|
||||
private @Nullable String name;
|
||||
private @Nullable HardwareVendor vendor;
|
||||
private @Nullable Type type;
|
||||
private @Nullable String driver;
|
||||
private @Nullable String driverVersion;
|
||||
private @Nullable List<Path> vulkanDriverFiles;
|
||||
|
||||
public GraphicsCard build() {
|
||||
String name = this.name;
|
||||
@@ -111,10 +122,12 @@ public final class GraphicsCard {
|
||||
name = "Unknown";
|
||||
}
|
||||
|
||||
return new GraphicsCard(name, vendor, type, driver, driverVersion);
|
||||
List<Path> vulkanDriverFiles = Objects.requireNonNullElse(this.vulkanDriverFiles, List.of());
|
||||
|
||||
return new GraphicsCard(name, vendor, type, driver, driverVersion, List.copyOf(vulkanDriverFiles));
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
public @Nullable String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -123,7 +136,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public HardwareVendor getVendor() {
|
||||
public @Nullable HardwareVendor getVendor() {
|
||||
return vendor;
|
||||
}
|
||||
|
||||
@@ -132,7 +145,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
public @Nullable Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@@ -141,7 +154,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDriver() {
|
||||
public @Nullable String getDriver() {
|
||||
return driver;
|
||||
}
|
||||
|
||||
@@ -150,7 +163,7 @@ public final class GraphicsCard {
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getDriverVersion() {
|
||||
public @Nullable String getDriverVersion() {
|
||||
return driverVersion;
|
||||
}
|
||||
|
||||
@@ -158,5 +171,14 @@ public final class GraphicsCard {
|
||||
this.driverVersion = driverVersion;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @Nullable List<Path> getVulkanDriverFiles() {
|
||||
return vulkanDriverFiles;
|
||||
}
|
||||
|
||||
public Builder setVulkanDriverFiles(@Nullable List<Path> files) {
|
||||
this.vulkanDriverFiles = files;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.platform.macos;
|
||||
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/// @author Glavo
|
||||
@NotNullByDefault
|
||||
public final class HomebrewUtils {
|
||||
|
||||
/// The path to Homebrew prefix.
|
||||
///
|
||||
/// - For macOS x86_64, it is `/usr/local`;
|
||||
/// - For macOS ARM64, it is `/opt/homebrew`;
|
||||
/// - For other operating systems, it is undefined.
|
||||
public static final Path HOMEBREW_PREFIX;
|
||||
|
||||
/// The path to `libvulkan.1.dylib`.
|
||||
///
|
||||
/// For non-macOS operating systems, it is undefined.
|
||||
public static final Path LIB_VULKAN;
|
||||
|
||||
static {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
|
||||
// TODO: custom Homebrew prefix for macOS
|
||||
HOMEBREW_PREFIX = Architecture.SYSTEM_ARCH.isX86()
|
||||
? Path.of("/usr/local")
|
||||
: Path.of("/opt/homebrew");
|
||||
|
||||
LIB_VULKAN = HOMEBREW_PREFIX.resolve("lib/libvulkan.1.dylib");
|
||||
} else {
|
||||
// For other operating systems, we don't need Homebrew.
|
||||
var placeholder = Path.of("");
|
||||
HOMEBREW_PREFIX = placeholder;
|
||||
LIB_VULKAN = placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
private HomebrewUtils() {
|
||||
}
|
||||
}
|
||||
+25
@@ -28,8 +28,11 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
|
||||
|
||||
@@ -48,6 +51,25 @@ final class WindowsGPUDetector {
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable List<Path> parseVulkanDriverFiles(Object vulkanDriverFiles) {
|
||||
if (vulkanDriverFiles instanceof String file) {
|
||||
try {
|
||||
return List.of(Path.of(file));
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to parse Vulkan ICD file path: " + file, e);
|
||||
return null;
|
||||
}
|
||||
} else if (vulkanDriverFiles instanceof String[] files) {
|
||||
try {
|
||||
return Stream.of(files).map(Path::of).toList();
|
||||
} catch (InvalidPathException e) {
|
||||
LOG.warning("Failed to parse Vulkan ICD file paths: " + Arrays.toString(files), e);
|
||||
return null;
|
||||
}
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<GraphicsCard> detectByCim() {
|
||||
try {
|
||||
String getCimInstance = OperatingSystem.SYSTEM_VERSION.getVersion().startsWith("6.1")
|
||||
@@ -116,6 +138,7 @@ final class WindowsGPUDetector {
|
||||
String vendor = regValueToString(reg.queryValue(hkey, subkey, "ProviderName"));
|
||||
String driverVersion = regValueToString(reg.queryValue(hkey, subkey, "DriverVersion"));
|
||||
String dacType = regValueToString(reg.queryValue(hkey, subkey, "HardwareInformation.DacType"));
|
||||
Object vulkanDriverName = reg.queryValue(hkey, subkey, "VulkanDriverName");
|
||||
|
||||
GraphicsCard.Builder builder = GraphicsCard.builder();
|
||||
if (name != null)
|
||||
@@ -126,6 +149,8 @@ final class WindowsGPUDetector {
|
||||
builder.setDriverVersion(driverVersion);
|
||||
if (dacType != null)
|
||||
builder.setType(fromDacType(dacType));
|
||||
if (vulkanDriverName != null)
|
||||
builder.setVulkanDriverFiles(parseVulkanDriverFiles(vulkanDriverName));
|
||||
result.add(builder.build());
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -914,4 +914,6 @@
|
||||
26.1
|
||||
26.1.1-rc-1
|
||||
26.1.1
|
||||
26.1.2-rc-1
|
||||
26.1.2
|
||||
26w14a
|
||||
Reference in new issue
Block a user