Compare commits
88
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9706a799be | ||
|
|
ec37b32eaa | ||
|
|
f893eeea1f | ||
|
|
8d559dba02 | ||
|
|
d167c712a6 | ||
|
|
8b224cb240 | ||
|
|
651aedaa50 | ||
|
|
a36b0ebed2 | ||
|
|
5a9e8683e4 | ||
|
|
843b29eff0 | ||
|
|
7bad0be2c4 | ||
|
|
5561011122 | ||
|
|
192a0c987e | ||
|
|
85a9a21e57 | ||
|
|
610864d674 | ||
|
|
06d0fa6e22 | ||
|
|
fb9a6fd314 | ||
|
|
5cab8ad41e | ||
|
|
d9f2b964c9 | ||
|
|
ac78b69ec2 | ||
|
|
516975688f | ||
|
|
7f07439388 | ||
|
|
bf87c4ac52 | ||
|
|
272132af51 | ||
|
|
04dd179631 | ||
|
|
57554c8d4b | ||
|
|
74a20bf11d | ||
|
|
16c96419d3 | ||
|
|
0b444bc5a2 | ||
|
|
7846e9e196 | ||
|
|
a74b971b98 | ||
|
|
6707ce1ed9 | ||
|
|
de421f93b5 | ||
|
|
1762e78cb3 | ||
|
|
2631f3a007 | ||
|
|
f70ec8a090 | ||
|
|
a4f22671c6 | ||
|
|
71c23df971 | ||
|
|
1bb2593c5b | ||
|
|
45d5ef4602 | ||
|
|
7b5b2596b9 | ||
|
|
23a805d576 | ||
|
|
f8cf3c22a1 | ||
|
|
8e5725946c | ||
|
|
99bb0d7a93 | ||
|
|
9695ddf8de | ||
|
|
50cff0bcba | ||
|
|
1832cf5924 | ||
|
|
10fba0a2b2 | ||
|
|
e2050fd47a | ||
|
|
f72da5973f | ||
|
|
0ebeffac32 | ||
|
|
dcfe724803 | ||
|
|
821020e7b6 | ||
|
|
33efc38f4c | ||
|
|
f01c0408e1 | ||
|
|
365ae0ef8c | ||
|
|
a6e593e3a1 | ||
|
|
67d38d6333 | ||
|
|
513d101fc4 | ||
|
|
5e1304f699 | ||
|
|
955a06d689 | ||
|
|
59e1a470e3 | ||
|
|
16952f5695 | ||
|
|
67feca075b | ||
|
|
12368375cf | ||
|
|
284ffb7aa0 | ||
|
|
1aefb26575 | ||
|
|
670d0f8462 | ||
|
|
3bbe422421 | ||
|
|
d68e988c15 | ||
|
|
74fd35047c | ||
|
|
21a9d19105 | ||
|
|
f662e983a4 | ||
|
|
7db379492a | ||
|
|
4a82daccec | ||
|
|
21e44bbc90 | ||
|
|
42a40a844a | ||
|
|
cd7c515097 | ||
|
|
9595626a27 | ||
|
|
db105c90b5 | ||
|
|
74a305c32a | ||
|
|
3af115ce37 | ||
|
|
b4e7934396 | ||
|
|
0a54037460 | ||
|
|
dd8da57cfb | ||
|
|
9b6f2d00ca | ||
|
|
2852821d2c |
No files matched your search
@@ -0,0 +1,67 @@
|
||||
name: Check Update
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- javafx
|
||||
|
||||
jobs:
|
||||
check-update:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HMCL_BASE_VERSION: '3.4'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
run: sudo apt-get install -y jq
|
||||
- name: Fetch last version
|
||||
run: |
|
||||
wget -O ci.json https://ci.huangyuhui.net/job/HMCL/lastSuccessfulBuild/api/json
|
||||
|
||||
export HMCL_BUILD_NUMBER=`cat ci.json | jq -M -r '.id'`
|
||||
export HMCL_VERSION="$HMCL_BASE_VERSION.$HMCL_BUILD_NUMBER"
|
||||
export HMCL_COMMIT_SHA=`cat ci.json | jq -M -r '.actions[] | select(._class == "hudson.plugins.git.util.BuildData") | .lastBuiltRevision.SHA1'`
|
||||
|
||||
if [ "${#HMCL_BUILD_NUMBER}" != 3 ]; then exit 1; fi
|
||||
if [ "${#HMCL_COMMIT_SHA}" != 40 ]; then exit 1; fi
|
||||
|
||||
echo "HMCL_BUILD_NUMBER=$HMCL_BUILD_NUMBER" >> $GITHUB_ENV
|
||||
echo "HMCL_VERSION=$HMCL_VERSION" >> $GITHUB_ENV
|
||||
echo "HMCL_COMMIT_SHA=$HMCL_COMMIT_SHA" >> $GITHUB_ENV
|
||||
echo "HMCL_TAG_NAME=v$HMCL_VERSION" >> $GITHUB_ENV
|
||||
- name: Check for existing tags
|
||||
run: if [ -z "$(git tag -l "$HMCL_TAG_NAME")" ]; then echo "continue=true" >> $GITHUB_ENV; fi
|
||||
- name: Download artifacts
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.exe.sha1"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar"
|
||||
wget "$DOWNLOAD_BASE_URL/HMCL-$HMCL_VERSION.jar.sha1"
|
||||
env:
|
||||
DOWNLOAD_BASE_URL: https://ci.huangyuhui.net/job/HMCL/${{ env.HMCL_BUILD_NUMBER }}/artifact/HMCL/build/libs
|
||||
- name: Generate release note
|
||||
if: ${{ env.continue == 'true' }}
|
||||
run: |
|
||||
echo "HMCL v$HMCL_VERSION" >> RELEASE_NOTE
|
||||
echo "" >> RELEASE_NOTE
|
||||
echo "The full changelogs can be found on the website: https://hmcl.huangyuhui.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
body_path: RELEASE_NOTE
|
||||
files: |
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe
|
||||
HMCL-${{ env.HMCL_VERSION }}.exe.sha1
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar
|
||||
HMCL-${{ env.HMCL_VERSION }}.jar.sha1
|
||||
target_commitish: ${{ env.HMCL_COMMIT_SHA }}
|
||||
name: ${{ env.HMCL_TAG_NAME }}
|
||||
tag_name: ${{ env.HMCL_TAG_NAME }}
|
||||
|
||||
|
||||
@@ -12,15 +12,17 @@ jobs:
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v2
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: HMCL
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
+73
-24
@@ -47,8 +47,9 @@ mainClassName = 'org.jackhuang.hmcl.Main'
|
||||
|
||||
dependencies {
|
||||
implementation project(":HMCLCore")
|
||||
implementation project(":JSTUN")
|
||||
implementation rootProject.files("lib/JFoenix.jar")
|
||||
|
||||
implementation group: 'de.javawi.jstun', name: 'jstun', version: '0.7.4'
|
||||
}
|
||||
|
||||
def digest(String algorithm, byte[] bytes) {
|
||||
@@ -117,13 +118,8 @@ compileJava11Java {
|
||||
targetCompatibility = 11
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn shadowJar
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
classifier = null
|
||||
classifier = "shadow"
|
||||
|
||||
manifest {
|
||||
attributes 'Created-By': 'Copyright(c) 2013-2021 huangyuhui.',
|
||||
@@ -156,12 +152,6 @@ shadowJar {
|
||||
'javafx.base/com.sun.javafx.event'
|
||||
].join(" ")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
@@ -170,15 +160,29 @@ task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
|
||||
outjars "${buildDir}/libs/${project.name}-${project.version}-proguard.jar"
|
||||
|
||||
|
||||
dontobfuscate
|
||||
dontoptimize
|
||||
keep 'public class org.jackhuang.**'
|
||||
keep 'public class org.slf4j.**'
|
||||
|
||||
keep 'class org.jackhuang.** { *; }'
|
||||
keep 'class com.jfoenix.** { *; }'
|
||||
keep 'class com.google.gson.** { *; }'
|
||||
keep 'class com.github.steveice10.opennbt.** { *; }' // TagRegistry use reflection to create object instance
|
||||
|
||||
keep 'class * extends com.google.gson.TypeAdapter'
|
||||
keep 'class * implements com.google.gson.TypeAdapterFactory'
|
||||
keep 'class * implements com.google.gson.JsonSerializer'
|
||||
keep 'class * implements com.google.gson.JsonDeserializer'
|
||||
|
||||
printusage "${buildDir}/libs/proguard-shrinking-details.txt"
|
||||
|
||||
System.out.println(new File(".").absolutePath)
|
||||
|
||||
dontwarn 'com.nqzero.**'
|
||||
dontwarn 'org.slf4j.**'
|
||||
dontwarn 'org.jackhuang.hmcl.util.Pack200Utils'
|
||||
dontwarn 'com.sun.javafx.**'
|
||||
dontwarn 'com.jfoenix.**'
|
||||
dontwarn 'org.apache.**'
|
||||
|
||||
adaptclassstrings
|
||||
|
||||
@@ -187,16 +191,61 @@ task proguard(type: proguard.gradle.ProGuardTask) {
|
||||
libraryjars files(configurations.compileClasspath.collect())
|
||||
|
||||
keepattributes 'SourceFile,LineNumberTable'
|
||||
keepattributes '*Annotation*'
|
||||
keepattributes 'Signature'
|
||||
|
||||
var javaHome = System.getProperty('java.home')
|
||||
|
||||
// Automatically handle the Java version of this build.
|
||||
// if (System.getProperty('java.version').startsWith('1.')) {
|
||||
// // Before Java 9, the runtime classes were packaged in a single jar file.
|
||||
libraryjars "${System.getProperty('java.home')}/lib/rt.jar"
|
||||
libraryjars "${System.getProperty('java.home')}/lib/ext/jfxrt.jar"
|
||||
// } else {
|
||||
// // As of Java 9, the runtime classes are packaged in modular jmod files.
|
||||
// libraryjars "${System.getProperty('java.home')}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
// }
|
||||
if (System.getProperty('java.version').startsWith('1.')) {
|
||||
// Before Java 9, the runtime classes were packaged in a single jar file.
|
||||
libraryjars "${javaHome}/lib/rt.jar"
|
||||
libraryjars "${javaHome}/lib/ext/jfxrt.jar"
|
||||
} else {
|
||||
// As of Java 9, the runtime classes are packaged in modular jmod files.
|
||||
libraryjars "${javaHome}/jmods/java.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.desktop.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.logging.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.sql.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/java.xml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
libraryjars "${javaHome}/jmods/jdk.management.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/jdk.unsupported.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
|
||||
if (new File("${javaHome}/jmods/javafx.base.jmod").exists()) {
|
||||
libraryjars "${javaHome}/jmods/javafx.base.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.controls.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.graphics.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.media.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.fxml.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
libraryjars "${javaHome}/jmods/javafx.web.jmod", jarfilter: '!**.jar', filter: '!module-info.class'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
task finalJar(type: Jar) {
|
||||
dependsOn proguard
|
||||
|
||||
manifest = shadowJar.manifest
|
||||
|
||||
from { proguard.outJarFiles.collect { zipTree(it)} }
|
||||
|
||||
into('/') {
|
||||
from { shadowJar.outputs.files.collect { zipTree(it) } }
|
||||
include("META-INF/versions/**")
|
||||
}
|
||||
|
||||
doLast {
|
||||
repack(jar.archivePath) // see repack()
|
||||
attachSignature(jar.archivePath)
|
||||
createChecksum(jar.archivePath)
|
||||
}
|
||||
}
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
dependsOn finalJar
|
||||
}
|
||||
|
||||
def createExecutable(String suffix, String header) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import javafx.scene.image.Image;
|
||||
import javafx.scene.shape.Mesh;
|
||||
import javafx.scene.shape.MeshView;
|
||||
import javafx.scene.shape.TriangleMesh;
|
||||
import org.jackhuang.hmcl.util.ArrayUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
|
||||
public class SkinCube extends MeshView {
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.setting.SambaException;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
@@ -31,6 +32,7 @@ import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
@@ -61,6 +63,8 @@ public final class Launcher extends Application {
|
||||
try {
|
||||
try {
|
||||
ConfigHolder.init();
|
||||
} catch (SambaException ignored) {
|
||||
Main.showWarningAndContinue(i18n("fatal.samba"));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load config", e);
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize()));
|
||||
@@ -106,7 +110,9 @@ public final class Launcher extends Application {
|
||||
|
||||
try {
|
||||
LOG.info("*** " + Metadata.TITLE + " ***");
|
||||
LOG.info("Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION);
|
||||
LOG.info("System Architecture: " + Architecture.SYSTEM_ARCH_NAME);
|
||||
LOG.info("Java Architecture: " + Architecture.CURRENT_ARCH_NAME);
|
||||
LOG.info("Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor"));
|
||||
LOG.info("Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor"));
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
@@ -116,7 +122,7 @@ public final class Launcher extends Application {
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
|
||||
launch(args);
|
||||
launch(Launcher.class, args);
|
||||
} catch (Throwable e) { // Fucking JavaFX will suppress the exception and will break our crash reporter.
|
||||
CRASH_REPORTER.uncaughtException(Thread.currentThread(), e);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@ package org.jackhuang.hmcl;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -33,7 +35,6 @@ import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.logging.Level;
|
||||
@@ -60,10 +61,7 @@ public final class Main {
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(() -> {
|
||||
fixLetsEncrypt();
|
||||
checkDSTRootCAX3();
|
||||
}, "CA Certificate Check", true);
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
@@ -106,28 +104,6 @@ public final class Main {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkDSTRootCAX3() {
|
||||
TrustManagerFactory tmf;
|
||||
try {
|
||||
tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init((KeyStore) null);
|
||||
} catch (NoSuchAlgorithmException | KeyStoreException e) {
|
||||
LOG.log(Level.WARNING, "Failed to init TrustManagerFactory", e);
|
||||
// don't know what to do here
|
||||
return;
|
||||
}
|
||||
for (TrustManager tm : tmf.getTrustManagers()) {
|
||||
if (tm instanceof X509TrustManager) {
|
||||
for (X509Certificate cert : ((X509TrustManager) tm).getAcceptedIssuers()) {
|
||||
if ("CN=DST Root CA X3, O=Digital Signature Trust Co.".equals((cert.getSubjectDN().getName()))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
showWarningAndContinue(i18n("fatal.missing_dst_root_ca_x3"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates that a fatal error has occurred, and that the application cannot start.
|
||||
*/
|
||||
@@ -157,8 +133,9 @@ public final class Main {
|
||||
}
|
||||
|
||||
KeyStore letsEncryptKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks");
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
try (InputStream letsEncryptFile = Main.class.getResourceAsStream("/assets/lekeystore.jks")) {
|
||||
letsEncryptKeyStore.load(letsEncryptFile, "supersecretpassword".toCharArray());
|
||||
}
|
||||
|
||||
KeyStore merged = KeyStore.getInstance(KeyStore.getDefaultType());
|
||||
merged.load(null, new char[0]);
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
@@ -137,15 +138,26 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
|
||||
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
|
||||
File srcDir = getVersionRoot(srcId);
|
||||
File dstDir = getVersionRoot(dstId);
|
||||
Path srcDir = getVersionRoot(srcId).toPath();
|
||||
Path dstDir = getVersionRoot(dstId).toPath();
|
||||
|
||||
if (dstDir.exists()) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir.toPath(), dstDir.toPath());
|
||||
if (Files.exists(dstDir.toPath().resolve(srcId + ".jar"))) {
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".jar"), dstDir.toPath().resolve(dstId + ".jar"));
|
||||
Version fromVersion = getVersion(srcId);
|
||||
|
||||
if (Files.exists(dstDir)) throw new IOException("Version exists");
|
||||
FileUtils.copyDirectory(srcDir, dstDir);
|
||||
|
||||
Path fromJson = dstDir.resolve(srcId + ".json");
|
||||
Path fromJar = dstDir.resolve(srcId + ".jar");
|
||||
Path toJson = dstDir.resolve(dstId + ".json");
|
||||
Path toJar = dstDir.resolve(dstId + ".jar");
|
||||
|
||||
if (Files.exists(fromJar)) {
|
||||
Files.move(fromJar, toJar);
|
||||
}
|
||||
Files.move(dstDir.toPath().resolve(srcId + ".json"), dstDir.toPath().resolve(dstId + ".json"));
|
||||
Files.move(fromJson, toJson);
|
||||
|
||||
FileUtils.writeText(toJson.toFile(), JsonUtils.GSON.toJson(fromVersion.setId(dstId)));
|
||||
|
||||
VersionSetting oldVersionSetting = getVersionSetting(srcId).clone();
|
||||
GameDirectoryType originalGameDirType = oldVersionSetting.getGameDirType();
|
||||
oldVersionSetting.setUsesGlobal(false);
|
||||
|
||||
@@ -23,12 +23,12 @@ import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.FileSystem;
|
||||
|
||||
/**
|
||||
* @author huangyuhui
|
||||
@@ -40,20 +40,20 @@ public final class HMCLModpackManager {
|
||||
/**
|
||||
* Read the manifest in a HMCL modpack.
|
||||
*
|
||||
* @param file a HMCL modpack file.
|
||||
* @param fs a HMCL modpack file.
|
||||
* @param encoding encoding of modpack zip file.
|
||||
* @return the manifest of HMCL modpack.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readHMCLModpackManifest(Path file, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json", encoding);
|
||||
public static Modpack readHMCLModpackManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String manifestJson = FileUtils.readText(fs.getPath("modpack.json"));
|
||||
Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding);
|
||||
String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json", encoding);
|
||||
String gameJson = FileUtils.readText(fs.getPath("minecraft/pack.json"));
|
||||
Version game = JsonUtils.fromNonNullJson(gameJson, Version.class);
|
||||
if (game.getJar() == null)
|
||||
if (StringUtils.isBlank(manifest.getVersion()))
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack " + file + ".");
|
||||
throw new JsonParseException("Cannot recognize the game version of modpack");
|
||||
else
|
||||
manifest.setManifest(HMCLModpackManifest.INSTANCE);
|
||||
else
|
||||
|
||||
@@ -17,22 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.MaintainTask;
|
||||
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
|
||||
import org.jackhuang.hmcl.download.game.GameVerificationFixTask;
|
||||
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
|
||||
import org.jackhuang.hmcl.download.java.JavaRepository;
|
||||
import org.jackhuang.hmcl.launch.NotDecompressingNativesException;
|
||||
import org.jackhuang.hmcl.launch.PermissionException;
|
||||
import org.jackhuang.hmcl.launch.ProcessCreationException;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.launch.*;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionException;
|
||||
import org.jackhuang.hmcl.mod.curse.CurseCompletionTask;
|
||||
@@ -41,15 +38,14 @@ import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.mcbbs.McbbsModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackCompletionTask;
|
||||
import org.jackhuang.hmcl.mod.server.ServerModpackLocalInstallTask;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.VersionSetting;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.io.ResponseCodeException;
|
||||
@@ -61,6 +57,7 @@ import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -68,6 +65,7 @@ import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -92,7 +90,8 @@ public final class LauncherHelper {
|
||||
this.launchingStepsPane.setTitle(i18n("version.launch"));
|
||||
}
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {});
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
@@ -283,6 +282,25 @@ public final class LauncherHelper {
|
||||
message = i18n("download.code.404", url);
|
||||
else
|
||||
message = i18n("download.failed", url, responseCode);
|
||||
} else if (ex instanceof CommandTooLongException) {
|
||||
message = i18n("launch.failed.command_too_long");
|
||||
} else if (ex instanceof ExecutionPolicyLimitException) {
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("launch.failed.execution_policy"),
|
||||
(result, resolve, reject) -> {
|
||||
if (CommandBuilder.setExecutionPolicy()) {
|
||||
LOG.info("Set the ExecutionPolicy for the scope 'CurrentUser' to 'RemoteSigned'");
|
||||
resolve.run();
|
||||
} else {
|
||||
LOG.warning("Failed to set ExecutionPolicy");
|
||||
reject.accept(i18n("launch.failed.execution_policy.failed_to_set"));
|
||||
}
|
||||
})
|
||||
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("launch.failed.execution_policy.hint")))
|
||||
);
|
||||
|
||||
return;
|
||||
} else if (ex instanceof AccessDeniedException) {
|
||||
message = i18n("exception.access_denied", ((AccessDeniedException) ex).getFile());
|
||||
} else {
|
||||
message = StringUtils.getStackTrace(ex);
|
||||
}
|
||||
@@ -318,9 +336,28 @@ public final class LauncherHelper {
|
||||
Runnable continueAction = () -> future.complete(JavaVersion.fromCurrentEnvironment());
|
||||
|
||||
if (setting.isJavaAutoSelected()) {
|
||||
// JavaVersionConstraint.VersionRange range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
|
||||
// TODO: download java 16 if necessary!
|
||||
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
JavaVersionConstraint.VersionRanges range = JavaVersionConstraint.findSuitableJavaVersionRange(gameVersion, version);
|
||||
GameJavaVersion targetJavaVersion;
|
||||
|
||||
if (range.getMandatory().contains(VersionNumber.asVersion("16"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_16;
|
||||
} else if (range.getMandatory().contains(VersionNumber.asVersion("1.8.0_51"))) {
|
||||
targetJavaVersion = GameJavaVersion.JAVA_8;
|
||||
} else {
|
||||
targetJavaVersion = null;
|
||||
}
|
||||
|
||||
if (targetJavaVersion != null) {
|
||||
downloadJava(gameVersion.toString(), targetJavaVersion, profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
future.complete(downloadedJavaVersion);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
Controllers.dialog(i18n("launch.failed.no_accepted_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Controllers.dialog(i18n("launch.wrong_javadir"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
|
||||
@@ -342,9 +379,9 @@ public final class LauncherHelper {
|
||||
|
||||
JavaVersionConstraint violatedMandatoryConstraint = null;
|
||||
JavaVersionConstraint violatedSuggestedConstraint = null;
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.values()) {
|
||||
if (constraint.getGameVersion().contains(gameVersion) && constraint.appliesToVersion(gameVersion, version)) {
|
||||
if (!constraint.getJavaVersion(version).contains(javaVersion.getVersionNumber())) {
|
||||
for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) {
|
||||
if (constraint.appliesToVersion(gameVersion, version, javaVersion)) {
|
||||
if (!constraint.checkJava(gameVersion, version, javaVersion)) {
|
||||
if (constraint.getType() == JavaVersionConstraint.RULE_MANDATORY) {
|
||||
violatedMandatoryConstraint = constraint;
|
||||
} else if (constraint.getType() == JavaVersionConstraint.RULE_SUGGESTED) {
|
||||
@@ -372,59 +409,47 @@ public final class LauncherHelper {
|
||||
} else {
|
||||
switch (violatedMandatoryConstraint) {
|
||||
case GAME_JSON:
|
||||
MessageDialogPane dialog = new MessageDialogPane(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion.toString(),
|
||||
version.getJavaVersion().getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION);
|
||||
|
||||
JFXButton linkButton = new JFXButton(i18n("download.external_link"));
|
||||
linkButton.setOnAction(e -> FXUtils.openLink("https://adoptopenjdk.net/"));
|
||||
linkButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(linkButton);
|
||||
|
||||
JFXButton yesButton = new JFXButton(i18n("button.ok"));
|
||||
yesButton.setOnAction(event -> {
|
||||
downloadJava(version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(x -> {
|
||||
try {
|
||||
Optional<JavaVersion> newAcceptableJava = JavaVersion.getJavas().stream()
|
||||
.filter(newJava -> newJava.getParsedVersion() >= version.getJavaVersion().getMajorVersion())
|
||||
.max(Comparator.comparing(JavaVersion::getVersionNumber));
|
||||
if (newAcceptableJava.isPresent()) {
|
||||
setting.setJavaVersion(newAcceptableJava.get());
|
||||
future.complete(newAcceptableJava.get());
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
LOG.log(Level.SEVERE, "Cannot list javas", e);
|
||||
}
|
||||
future.complete(javaVersion);
|
||||
}, Platform::runLater)
|
||||
.exceptionally(Lang.handleUncaught);
|
||||
});
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
dialog.addButton(yesButton);
|
||||
|
||||
JFXButton noButton = new JFXButton(i18n("button.cancel"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
dialog.addButton(noButton);
|
||||
dialog.setCancelButton(noButton);
|
||||
|
||||
Controllers.dialog(dialog);
|
||||
downloadJava(gameVersion.toString(), version.getJavaVersion(), profile)
|
||||
.thenAcceptAsync(downloadedJavaVersion -> {
|
||||
setting.setJavaVersion(downloadedJavaVersion);
|
||||
future.complete(downloadedJavaVersion);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
breakAction.run();
|
||||
return null;
|
||||
});
|
||||
return Task.fromCompletableFuture(future);
|
||||
case VANILLA_JAVA_16:
|
||||
Controllers.confirm(i18n("launch.advice.require_newer_java_version", gameVersion.toString(), 16), i18n("message.warning"), () -> {
|
||||
FXUtils.openLink("https://adoptopenjdk.net/");
|
||||
FXUtils.openLink("https://adoptium.net/?variant=openjdk17");
|
||||
}, breakAction);
|
||||
return null;
|
||||
case VANILLA_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.java8_1_13"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
case VANILLA_LINUX_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_linux_java_8"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case VANILLA_X86:
|
||||
if (setting.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX
|
||||
// Windows on ARM introduced translation support for x86-64 after 10.0.21277.
|
||||
|| (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277)) {
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86.translation"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Controllers.dialog(i18n("launch.advice.vanilla_x86"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
case LAUNCH_WRAPPER:
|
||||
Controllers.dialog(i18n("launch.advice.java9") + "\n" + i18n("launch.advice.uncorrected"), i18n("message.error"), MessageType.ERROR, breakAction);
|
||||
return null;
|
||||
@@ -432,13 +457,14 @@ public final class LauncherHelper {
|
||||
}
|
||||
}
|
||||
|
||||
if (javaVersion.getPlatform() != Architecture.SYSTEM.getPlatform()) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64
|
||||
&& javaVersion.getPlatform().getArchitecture() == Architecture.X86) {
|
||||
Controllers.dialog(i18n("launch.advice.different_platform"), i18n("message.warning"), MessageType.ERROR, continueAction);
|
||||
suggested = true;
|
||||
}
|
||||
|
||||
// 32-bit JVM cannot make use of too much memory.
|
||||
if (javaVersion.getPlatform() == org.jackhuang.hmcl.util.platform.Platform.BIT_32 &&
|
||||
if (javaVersion.getBits() == Bits.BIT_32 &&
|
||||
setting.getMaxMemory() > 1.5 * 1024) {
|
||||
// 1.5 * 1024 is an inaccurate number.
|
||||
// Actual memory limit depends on operating system and memory.
|
||||
@@ -450,7 +476,7 @@ public final class LauncherHelper {
|
||||
suggested = true;
|
||||
switch (violatedSuggestedConstraint) {
|
||||
case MODDED_JAVA_7:
|
||||
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.error"), MessageType.ERROR, continueAction);
|
||||
Controllers.dialog(i18n("launch.advice.java.modded_java_7"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
return null;
|
||||
case MODDED_JAVA_8:
|
||||
Controllers.dialog(i18n("launch.advice.newer_java"), i18n("message.warning"), MessageType.WARNING, continueAction);
|
||||
@@ -500,33 +526,70 @@ public final class LauncherHelper {
|
||||
}).withStage("launch.state.java");
|
||||
}
|
||||
|
||||
private static CompletableFuture<Void> downloadJava(GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
private static CompletableFuture<JavaVersion> downloadJava(String gameVersion, GameJavaVersion javaVersion, Profile profile) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("download.external_link"));
|
||||
link.setOnAction(e -> {
|
||||
FXUtils.openLink("https://docs.microsoft.com/zh-cn/java/openjdk/download");
|
||||
future.completeExceptionally(new CancellationException());
|
||||
});
|
||||
|
||||
Controllers.dialog(new MessageDialogPane.Builder(
|
||||
i18n("launch.advice.require_newer_java_version",
|
||||
gameVersion,
|
||||
javaVersion.getMajorVersion()),
|
||||
i18n("message.warning"),
|
||||
MessageType.QUESTION)
|
||||
.addAction(link)
|
||||
.yesOrNo(() -> {
|
||||
downloadJavaImpl(javaVersion, profile.getDependency().getDownloadProvider())
|
||||
.thenAcceptAsync(downloadedJava -> {
|
||||
future.complete(downloadedJava);
|
||||
})
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolvedException = resolveException(throwable);
|
||||
LOG.log(Level.WARNING, "Failed to download java", throwable);
|
||||
if (!(resolvedException instanceof CancellationException)) {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(resolvedException), i18n("install.failed"));
|
||||
}
|
||||
future.completeExceptionally(new CancellationException());
|
||||
return null;
|
||||
});
|
||||
}, () -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
}).build());
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly start java downloading.
|
||||
*
|
||||
* @param javaVersion target Java version
|
||||
* @param downloadProvider download provider
|
||||
* @return JavaVersion, null if we failed to download java, failed if an error occurred when downloading.
|
||||
*/
|
||||
private static CompletableFuture<JavaVersion> downloadJavaImpl(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
CompletableFuture<JavaVersion> future = new CompletableFuture<>();
|
||||
|
||||
TaskExecutorDialogPane javaDownloadingPane = new TaskExecutorDialogPane(it -> {
|
||||
});
|
||||
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion,
|
||||
profile.getDependency().getDownloadProvider()).executor(false);
|
||||
executor.addTaskListener(new TaskListener() {
|
||||
@Override
|
||||
public void onStop(boolean success, TaskExecutor executor) {
|
||||
super.onStop(success, executor);
|
||||
Platform.runLater(() -> {
|
||||
if (!success) {
|
||||
future.completeExceptionally(Optional.ofNullable(executor.getException()).orElseGet(InterruptedException::new));
|
||||
TaskExecutor executor = JavaRepository.downloadJava(javaVersion, downloadProvider)
|
||||
.whenComplete(Schedulers.javafx(), (downloadedJava, exception) -> {
|
||||
if (exception != null) {
|
||||
future.completeExceptionally(exception);
|
||||
} else {
|
||||
future.complete(null);
|
||||
future.complete(downloadedJava);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
})
|
||||
.executor(false);
|
||||
|
||||
javaDownloadingPane.setExecutor(executor, true);
|
||||
Controllers.dialog(javaDownloadingPane);
|
||||
executor.start();
|
||||
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -600,6 +663,11 @@ public final class LauncherHelper {
|
||||
|
||||
LOG.info("Launched process: " + command);
|
||||
|
||||
String classpath = process.getClasspath();
|
||||
if (classpath != null) {
|
||||
LOG.info("Process ClassPath: " + classpath);
|
||||
}
|
||||
|
||||
if (showLogs)
|
||||
Platform.runLater(() -> {
|
||||
logWindow = new LogWindow();
|
||||
@@ -695,6 +763,7 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
public static final Queue<ManagedProcess> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static void stopManagedProcesses() {
|
||||
while (!PROCESSES.isEmpty())
|
||||
Optional.ofNullable(PROCESSES.poll()).ifPresent(ManagedProcess::stop);
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -61,34 +62,38 @@ public final class ModpackHelper {
|
||||
private ModpackHelper() {}
|
||||
|
||||
public static Modpack readModpackManifest(Path file, Charset charset) throws UnsupportedModpackException, ManuallyCreatedModpackException {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
try (ZipFileSystem zfs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
try {
|
||||
return McbbsModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception ignored) {
|
||||
// ignore it, not a valid MCBBS modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
try {
|
||||
return CurseManifest.readCurseForgeModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid CurseForge modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
try {
|
||||
return HMCLModpackManager.readHMCLModpackManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid HMCL modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
try {
|
||||
return MultiMCInstanceConfiguration.readMultiMCModpackManifest(zfs, file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid MultiMC modpack.
|
||||
}
|
||||
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(file, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
try {
|
||||
return ServerModpackManifest.readManifest(zfs, charset);
|
||||
} catch (Exception e) {
|
||||
// ignore it, not a valid Server modpack.
|
||||
}
|
||||
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(file, charset)) {
|
||||
|
||||
+50
-7
@@ -19,7 +19,9 @@ package org.jackhuang.hmcl.game;
|
||||
|
||||
import fi.iki.elonen.NanoHTTPD;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
@@ -39,7 +41,7 @@ import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.thread;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class MicrosoftAuthenticationServer extends NanoHTTPD implements MicrosoftService.OAuthSession {
|
||||
public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
private final int port;
|
||||
private final CompletableFuture<String> future = new CompletableFuture<>();
|
||||
|
||||
@@ -47,7 +49,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
|
||||
private String idToken;
|
||||
|
||||
private MicrosoftAuthenticationServer(int port) {
|
||||
private OAuthServer(int port) {
|
||||
super(port);
|
||||
|
||||
this.port = port;
|
||||
@@ -102,7 +104,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(MicrosoftAuthenticationServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
@@ -119,10 +121,12 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
return newFixedLengthResponse(Response.Status.OK, "text/html; charset=UTF-8", html);
|
||||
}
|
||||
|
||||
public static class Factory implements MicrosoftService.OAuthCallback {
|
||||
public static class Factory implements OAuth.Callback {
|
||||
public final EventManager<GrantDeviceCodeEvent> onGrantDeviceCode = new EventManager<>();
|
||||
public final EventManager<OpenBrowserEvent> onOpenBrowser = new EventManager<>();
|
||||
|
||||
@Override
|
||||
public MicrosoftService.OAuthSession startServer() throws IOException, AuthenticationException {
|
||||
public OAuth.Session startServer() throws IOException, AuthenticationException {
|
||||
if (StringUtils.isBlank(getClientId())) {
|
||||
throw new MicrosoftAuthenticationNotSupportedException();
|
||||
}
|
||||
@@ -130,7 +134,7 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
IOException exception = null;
|
||||
for (int port : new int[]{29111, 29112, 29113, 29114, 29115}) {
|
||||
try {
|
||||
MicrosoftAuthenticationServer server = new MicrosoftAuthenticationServer(port);
|
||||
OAuthServer server = new OAuthServer(port);
|
||||
server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, true);
|
||||
return server;
|
||||
} catch (IOException e) {
|
||||
@@ -140,10 +144,17 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
throw exception;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void grantDeviceCode(String userCode, String verificationURI) {
|
||||
onGrantDeviceCode.fireEvent(new GrantDeviceCodeEvent(this, userCode, verificationURI));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void openBrowser(String url) throws IOException {
|
||||
lastlyOpenedURL = url;
|
||||
FXUtils.openLink(url);
|
||||
|
||||
onOpenBrowser.fireEvent(new OpenBrowserEvent(this, url));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,6 +171,38 @@ public final class MicrosoftAuthenticationServer extends NanoHTTPD implements Mi
|
||||
|
||||
}
|
||||
|
||||
public static class GrantDeviceCodeEvent extends Event {
|
||||
private final String userCode;
|
||||
private final String verificationUri;
|
||||
|
||||
public GrantDeviceCodeEvent(Object source, String userCode, String verificationUri) {
|
||||
super(source);
|
||||
this.userCode = userCode;
|
||||
this.verificationUri = verificationUri;
|
||||
}
|
||||
|
||||
public String getUserCode() {
|
||||
return userCode;
|
||||
}
|
||||
|
||||
public String getVerificationUri() {
|
||||
return verificationUri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class OpenBrowserEvent extends Event {
|
||||
private final String url;
|
||||
|
||||
public OpenBrowserEvent(Object source, String url) {
|
||||
super(source);
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MicrosoftAuthenticationNotSupportedException extends AuthenticationException {
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.util.skin.InvalidSkinException;
|
||||
|
||||
@@ -75,12 +75,12 @@ public final class Accounts {
|
||||
}
|
||||
}
|
||||
|
||||
public static final MicrosoftService.OAuthCallback MICROSOFT_OAUTH_CALLBACK = new MicrosoftAuthenticationServer.Factory();
|
||||
public static final OAuthServer.Factory OAUTH_CALLBACK = new OAuthServer.Factory();
|
||||
|
||||
public static final OfflineAccountFactory FACTORY_OFFLINE = new OfflineAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER);
|
||||
public static final YggdrasilAccountFactory FACTORY_MOJANG = YggdrasilAccountFactory.MOJANG;
|
||||
public static final AuthlibInjectorAccountFactory FACTORY_AUTHLIB_INJECTOR = new AuthlibInjectorAccountFactory(AUTHLIB_INJECTOR_DOWNLOADER, Accounts::getOrCreateAuthlibInjectorServer);
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(MICROSOFT_OAUTH_CALLBACK));
|
||||
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
|
||||
public static final List<AccountFactory<?>> FACTORIES = immutableListOf(FACTORY_OFFLINE, FACTORY_MOJANG, FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
|
||||
|
||||
// ==== login type / account factory mapping ====
|
||||
@@ -374,7 +374,7 @@ public final class Accounts {
|
||||
return i18n("account.methods.microsoft.error.no_character");
|
||||
} else if (exception instanceof MicrosoftService.NoXuiException) {
|
||||
return i18n("account.methods.microsoft.error.add_family_probably");
|
||||
} else if (exception instanceof MicrosoftAuthenticationServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
} else if (exception instanceof OAuthServer.MicrosoftAuthenticationNotSupportedException) {
|
||||
return i18n("account.methods.microsoft.snapshot");
|
||||
} else if (exception instanceof OAuthAccount.WrongAccountException) {
|
||||
return i18n("account.failed.wrong_account");
|
||||
|
||||
@@ -26,9 +26,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.*;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -37,7 +35,8 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public final class ConfigHolder {
|
||||
|
||||
private ConfigHolder() {}
|
||||
private ConfigHolder() {
|
||||
}
|
||||
|
||||
public static final String CONFIG_FILENAME = "hmcl.json";
|
||||
public static final String CONFIG_FILENAME_LINUX = ".hmcl.json";
|
||||
@@ -97,9 +96,16 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
if (!Files.isWritable(configLocation)) {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
&& configLocation.getFileSystem() == FileSystems.getDefault()
|
||||
&& configLocation.toFile().canWrite()) {
|
||||
// There are some serious problems with the implementation of Samba or OpenJDK
|
||||
throw new SambaException();
|
||||
} else {
|
||||
// the config cannot be saved
|
||||
// throw up the error now to prevent further data loss
|
||||
throw new IOException("Config at " + configLocation + " is not writable");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import java.nio.file.AccessDeniedException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -137,7 +138,7 @@ public final class DownloadProviders {
|
||||
return config().isAutoChooseDownloadType() ? currentDownloadProvider : fileDownloadProvider;
|
||||
}
|
||||
|
||||
public static String localizeErrorMessage(Exception exception) {
|
||||
public static String localizeErrorMessage(Throwable exception) {
|
||||
if (exception instanceof DownloadException) {
|
||||
URL url = ((DownloadException) exception).getUrl();
|
||||
if (exception.getCause() instanceof SocketTimeoutException) {
|
||||
@@ -152,10 +153,16 @@ public final class DownloadProviders {
|
||||
} else if (exception.getCause() instanceof FileNotFoundException) {
|
||||
return i18n("download.code.404", url);
|
||||
} else if (exception.getCause() instanceof AccessDeniedException) {
|
||||
return i18n("install.failed.downloading.access_denied", url, ((AccessDeniedException) exception.getCause()).getFile());
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.access_denied", ((AccessDeniedException) exception.getCause()).getFile());
|
||||
} else if (exception.getCause() instanceof ArtifactMalformedException) {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + i18n("exception.artifact_malformed");
|
||||
} else {
|
||||
return i18n("install.failed.downloading.detail", url) + "\n" + StringUtils.getStackTrace(exception.getCause());
|
||||
}
|
||||
} else if (exception instanceof ArtifactMalformedException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else if (exception instanceof CancellationException) {
|
||||
return i18n("message.cancelled");
|
||||
}
|
||||
return StringUtils.getStackTrace(exception);
|
||||
}
|
||||
|
||||
@@ -21,5 +21,6 @@ public enum EnumBackgroundImage {
|
||||
DEFAULT,
|
||||
CUSTOM,
|
||||
CLASSIC,
|
||||
NETWORK
|
||||
NETWORK,
|
||||
TRANSLUCENT
|
||||
}
|
||||
@@ -20,7 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import org.jackhuang.hmcl.auth.microsoft.MicrosoftService;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
@@ -57,11 +57,11 @@ public final class HMCLAccounts {
|
||||
String scope = "openid offline_access";
|
||||
|
||||
return Task.supplyAsync(() -> {
|
||||
MicrosoftService.OAuthSession session = Accounts.MICROSOFT_OAUTH_CALLBACK.startServer();
|
||||
Accounts.MICROSOFT_OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
OAuth.Session session = Accounts.OAUTH_CALLBACK.startServer();
|
||||
Accounts.OAUTH_CALLBACK.openBrowser(NetworkUtils.withQuery(
|
||||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
||||
mapOf(
|
||||
pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()),
|
||||
pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()),
|
||||
pair("response_type", "id_token code"),
|
||||
pair("response_mode", "form_post"),
|
||||
pair("scope", scope),
|
||||
@@ -72,12 +72,12 @@ public final class HMCLAccounts {
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST("https://login.microsoftonline.com/common/oauth2/v2.0/token")
|
||||
.form(mapOf(pair("client_id", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.MICROSOFT_OAUTH_CALLBACK.getClientSecret()),
|
||||
.form(mapOf(pair("client_id", Accounts.OAUTH_CALLBACK.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", Accounts.OAUTH_CALLBACK.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", scope)))
|
||||
.getString();
|
||||
MicrosoftService.LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
MicrosoftService.LiveAuthorizationResponse.class);
|
||||
OAuth.AuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
OAuth.AuthorizationResponse.class);
|
||||
|
||||
HMCLAccountProfile profile = HttpRequest.GET("https://hmcl.huangyuhui.net/api/user")
|
||||
.header("Token-Type", response.tokenType)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.setting;
|
||||
|
||||
public final class SambaException extends RuntimeException {
|
||||
public SambaException() {
|
||||
}
|
||||
|
||||
public SambaException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public SambaException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public SambaException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -55,7 +56,7 @@ public class Theme {
|
||||
|
||||
Theme(String name, String color) {
|
||||
this.name = name;
|
||||
this.color = color;
|
||||
this.color = Objects.requireNonNull(color);
|
||||
this.paint = Color.web(color);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -608,7 +609,7 @@ public final class VersionSetting implements Cloneable {
|
||||
if (checkJava)
|
||||
return JavaVersion.fromExecutable(Paths.get(getJavaDir()));
|
||||
else
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Architecture.SYSTEM.getPlatform());
|
||||
return new JavaVersion(Paths.get(getJavaDir()), "", Platform.getPlatform(OperatingSystem.CURRENT_OS, Architecture.UNKNOWN));
|
||||
} catch (IOException | InvalidPathException e) {
|
||||
return null; // Custom Java Directory not found,
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BooleanSupplier;
|
||||
@@ -333,7 +334,7 @@ public final class FXUtils {
|
||||
public static void playAnimation(Node node, String animationKey, Timeline timeline) {
|
||||
animationKey = "FXUTILS.ANIMATION." + animationKey;
|
||||
Object oldTimeline = node.getProperties().get(animationKey);
|
||||
if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop();
|
||||
if (timeline != null) timeline.play();
|
||||
node.getProperties().put(animationKey, timeline);
|
||||
}
|
||||
@@ -700,7 +701,7 @@ public final class FXUtils {
|
||||
Controllers.showToast(i18n("message.copied"));
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
@@ -719,6 +720,10 @@ public final class FXUtils {
|
||||
JFXHyperlink hyperlink = new JFXHyperlink(element.getTextContent());
|
||||
hyperlink.setOnAction(e -> hyperlinkAction.accept(href));
|
||||
texts.add(hyperlink);
|
||||
} else if ("b".equals(element.getTagName())) {
|
||||
Text text = new Text(element.getTextContent());
|
||||
text.getStyleClass().add("bold");
|
||||
texts.add(text);
|
||||
} else if ("br".equals(element.getTagName())) {
|
||||
texts.add(new Text("\n"));
|
||||
} else {
|
||||
@@ -728,12 +733,17 @@ public final class FXUtils {
|
||||
texts.add(new Text(node.getTextContent()));
|
||||
}
|
||||
}
|
||||
final TextFlow tf = new TextFlow(texts.toArray(new javafx.scene.Node[0]));
|
||||
return tf;
|
||||
return texts;
|
||||
} catch (SAXException | ParserConfigurationException | IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse xml", e);
|
||||
return new TextFlow(new Text(segment));
|
||||
return Collections.singletonList(new Text(segment));
|
||||
}
|
||||
}
|
||||
|
||||
public static TextFlow segmentToTextFlow(final String segment, Consumer<String> hyperlinkAction) {
|
||||
TextFlow tf = new TextFlow();
|
||||
tf.getChildren().setAll(parseSegment(segment, hyperlinkAction));
|
||||
return tf;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,8 +69,8 @@ public class GameCrashWindow extends Stage {
|
||||
private final String memory;
|
||||
private final String java;
|
||||
private final LibraryAnalyzer analyzer;
|
||||
private final StringProperty os = new SimpleStringProperty(System.getProperty("os.name"));
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCHITECTURE);
|
||||
private final StringProperty os = new SimpleStringProperty(OperatingSystem.SYSTEM_NAME);
|
||||
private final StringProperty arch = new SimpleStringProperty(Architecture.SYSTEM_ARCH.getDisplayName());
|
||||
private final StringProperty reason = new SimpleStringProperty(i18n("game.crash.reason.unknown"));
|
||||
private final BooleanProperty loading = new SimpleBooleanProperty();
|
||||
private final Label feedbackLabel = new Label(i18n("game.crash.feedback"));
|
||||
@@ -81,9 +81,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, LinkedList<Pair<String, Log4jLevel>> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@@ -93,7 +93,10 @@ public class GameCrashWindow extends Stage {
|
||||
this.analyzer = LibraryAnalyzer.analyze(version);
|
||||
|
||||
memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " MB").orElse("-");
|
||||
java = launchOptions.getJava().getVersion();
|
||||
|
||||
this.java = launchOptions.getJava().getArchitecture() == Architecture.SYSTEM_ARCH
|
||||
? launchOptions.getJava().getVersion()
|
||||
: launchOptions.getJava().getVersion() + " (" + launchOptions.getJava().getArchitecture().getDisplayName() + ")";
|
||||
|
||||
this.view = new View();
|
||||
|
||||
@@ -110,14 +113,18 @@ public class GameCrashWindow extends Stage {
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
String rawLog = logs.stream().map(Pair::getKey).collect(Collectors.joining("\n"));
|
||||
Set<String> keywords = Collections.emptySet();
|
||||
String crashReport = null;
|
||||
try {
|
||||
String crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
crashReport = CrashReportAnalyzer.findCrashReport(rawLog);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to read crash report", e);
|
||||
}
|
||||
if (crashReport == null) {
|
||||
crashReport = CrashReportAnalyzer.extractCrashReport(rawLog);
|
||||
}
|
||||
if (crashReport != null) {
|
||||
keywords = CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport);
|
||||
}
|
||||
return pair(
|
||||
CrashReportAnalyzer.anaylze(rawLog),
|
||||
keywords);
|
||||
@@ -151,6 +158,9 @@ public class GameCrashWindow extends Stage {
|
||||
translateFabricModId(result.getMatcher().group("mod")),
|
||||
result.getMatcher().group("version")));
|
||||
break;
|
||||
case TWILIGHT_FOREST_OPTIFINE:
|
||||
reasonText.append(i18n("game.crash.reason.mod", "OptiFine"));
|
||||
break;
|
||||
default:
|
||||
reasonText.append(i18n("game.crash.reason." + result.getRule().name().toLowerCase(Locale.ROOT),
|
||||
Arrays.stream(result.getRule().getGroupNames()).map(groupName -> result.getMatcher().group(groupName))
|
||||
@@ -207,6 +217,7 @@ public class GameCrashWindow extends Stage {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ import javafx.application.Platform;
|
||||
import javafx.beans.NamedArg;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -43,7 +45,7 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -53,6 +55,7 @@ import org.jackhuang.hmcl.task.TaskExecutor;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.UUIDTypeAdapter;
|
||||
@@ -88,6 +91,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
private final Pane detailsContainer;
|
||||
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
private TaskExecutor loginTask;
|
||||
|
||||
@@ -217,6 +222,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
logging.set(true);
|
||||
deviceCode.set(null);
|
||||
|
||||
loginTask = Task.supplyAsync(() -> factory.create(new DialogCharacterSelector(), username, password, null, additionalData))
|
||||
.whenComplete(Schedulers.javafx(), account -> {
|
||||
@@ -262,15 +268,23 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
if (factory == Accounts.FACTORY_MICROSOFT) {
|
||||
VBox vbox = new VBox(8);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(value -> {
|
||||
runInFX(() -> deviceCode.set(value));
|
||||
}));
|
||||
|
||||
HBox box = new HBox(8);
|
||||
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package org.jackhuang.hmcl.ui.account;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -9,13 +9,16 @@ import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.AuthInfo;
|
||||
import org.jackhuang.hmcl.auth.OAuthAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilService;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.ui.WeakListenerHolder;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.HintPane;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
@@ -27,7 +30,9 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
private final OAuthAccount account;
|
||||
private final Consumer<AuthInfo> success;
|
||||
private final Runnable failed;
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
private final ObjectProperty<OAuthServer.GrantDeviceCodeEvent> deviceCode = new SimpleObjectProperty<>();
|
||||
|
||||
private final WeakListenerHolder holder = new WeakListenerHolder();
|
||||
|
||||
public OAuthAccountLoginDialog(OAuthAccount account, Consumer<AuthInfo> success, Runnable failed) {
|
||||
this.account = account;
|
||||
@@ -40,13 +45,17 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
Label usernameLabel = new Label(account.getUsername());
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
FXUtils.onChangeAndOperate(deviceCode, deviceCode -> {
|
||||
if (deviceCode != null) {
|
||||
FXUtils.copyText(deviceCode.getUserCode());
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.manual", deviceCode.getUserCode(), deviceCode.getVerificationUri()));
|
||||
} else {
|
||||
hintPane.setSegment(i18n("account.methods.microsoft.hint"));
|
||||
}
|
||||
});
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
if (deviceCode.get() != null) {
|
||||
FXUtils.copyText(deviceCode.get().getUserCode());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -62,15 +71,21 @@ public class OAuthAccountLoginDialog extends DialogPane {
|
||||
|
||||
vbox.getChildren().setAll(usernameLabel, hintPane, box);
|
||||
setBody(vbox);
|
||||
|
||||
holder.add(Accounts.OAUTH_CALLBACK.onGrantDeviceCode.registerWeak(this::onGrantDeviceCode));
|
||||
}
|
||||
|
||||
private void onGrantDeviceCode(OAuthServer.GrantDeviceCodeEvent event) {
|
||||
FXUtils.runInFX(() -> {
|
||||
deviceCode.set(event);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAccept() {
|
||||
setLoading();
|
||||
logging.set(true);
|
||||
Task.supplyAsync(account::logInWhenCredentialsExpired)
|
||||
.whenComplete(Schedulers.javafx(), (authInfo, exception) -> {
|
||||
logging.set(false);
|
||||
if (exception == null) {
|
||||
success.accept(authInfo);
|
||||
onSuccess();
|
||||
|
||||
@@ -63,6 +63,12 @@ public class TransitionPane extends StackPane implements AnimationHandler {
|
||||
|
||||
updateContent(newView);
|
||||
|
||||
if (previousNode == EMPTY_PANE) {
|
||||
setMouseTransparent(false);
|
||||
getChildren().setAll(newView);
|
||||
return;
|
||||
}
|
||||
|
||||
transition.init(this);
|
||||
|
||||
// runLater or "init" will not work
|
||||
|
||||
@@ -19,37 +19,26 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class AdvancedListBox extends ScrollPane {
|
||||
private final VBox container = new VBox();
|
||||
public class AdvancedListBox extends VBox {
|
||||
|
||||
{
|
||||
setContent(container);
|
||||
|
||||
FXUtils.smoothScrolling(this);
|
||||
|
||||
setFitToHeight(true);
|
||||
setFitToWidth(true);
|
||||
setHbarPolicy(ScrollBarPolicy.NEVER);
|
||||
|
||||
container.getStyleClass().add("advanced-list-box-content");
|
||||
getStyleClass().add("advanced-list-box-content");
|
||||
}
|
||||
|
||||
public AdvancedListBox add(Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(child);
|
||||
getChildren().add(child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(pane);
|
||||
getChildren().add(pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -64,27 +53,27 @@ public class AdvancedListBox extends ScrollPane {
|
||||
|
||||
public AdvancedListBox add(int index, Node child) {
|
||||
if (child instanceof Pane || child instanceof AdvancedListItem)
|
||||
container.getChildren().add(index, child);
|
||||
getChildren().add(index, child);
|
||||
else {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("advanced-list-box-item");
|
||||
pane.getChildren().setAll(child);
|
||||
container.getChildren().add(index, pane);
|
||||
getChildren().add(index, pane);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public AdvancedListBox remove(Node child) {
|
||||
container.getChildren().remove(indexOf(child));
|
||||
getChildren().remove(indexOf(child));
|
||||
return this;
|
||||
}
|
||||
|
||||
public int indexOf(Node child) {
|
||||
if (child instanceof Pane) {
|
||||
return container.getChildren().indexOf(child);
|
||||
return getChildren().indexOf(child);
|
||||
} else {
|
||||
for (int i = 0; i < container.getChildren().size(); ++i) {
|
||||
Node node = container.getChildren().get(i);
|
||||
for (int i = 0; i < getChildren().size(); ++i) {
|
||||
Node node = getChildren().get(i);
|
||||
if (node instanceof StackPane) {
|
||||
ObservableList<Node> list = ((StackPane) node).getChildren();
|
||||
if (list.size() == 1 && list.get(0) == child)
|
||||
@@ -99,11 +88,7 @@ public class AdvancedListBox extends ScrollPane {
|
||||
return add(new ClassTitle(category));
|
||||
}
|
||||
|
||||
public void setSpacing(double spacing) {
|
||||
container.setSpacing(spacing);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
container.getChildren().clear();
|
||||
getChildren().clear();
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@ import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
|
||||
public class HintPane extends VBox {
|
||||
@@ -84,6 +86,10 @@ public class HintPane extends VBox {
|
||||
this.text.set(text);
|
||||
}
|
||||
|
||||
public void setSegment(String segment) {
|
||||
flow.getChildren().setAll(FXUtils.parseSegment(segment, Controllers::onHyperlinkAction));
|
||||
}
|
||||
|
||||
public void setChildren(Node... children) {
|
||||
flow.getChildren().setAll(children);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.construct;
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.HBox;
|
||||
@@ -99,7 +100,7 @@ public final class MessageDialogPane extends StackPane {
|
||||
});
|
||||
}
|
||||
|
||||
public void addButton(ButtonBase btn) {
|
||||
public void addButton(Node btn) {
|
||||
btn.addEventHandler(ActionEvent.ACTION, e -> fireEvent(new DialogCloseEvent()));
|
||||
actions.getChildren().add(btn);
|
||||
}
|
||||
@@ -119,6 +120,11 @@ public final class MessageDialogPane extends StackPane {
|
||||
this.dialog = new MessageDialogPane(text, title, type);
|
||||
}
|
||||
|
||||
public Builder addAction(Node actionNode) {
|
||||
dialog.addButton(actionNode);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder ok(Runnable ok) {
|
||||
JFXButton btnOk = new JFXButton(i18n("button.ok"));
|
||||
btnOk.getStyleClass().add("dialog-accept");
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -61,7 +62,7 @@ public class MultiFileItem<T> extends VBox {
|
||||
});
|
||||
selectedData.addListener((a, b, newValue) -> {
|
||||
Optional<Toggle> selecting = group.getToggles().stream()
|
||||
.filter(it -> it.getUserData() == newValue)
|
||||
.filter(it -> Objects.equals(it.getUserData(), newValue))
|
||||
.findFirst();
|
||||
if (!selecting.isPresent()) {
|
||||
selecting = group.getToggles().stream()
|
||||
@@ -226,6 +227,11 @@ public class MultiFileItem<T> extends VBox {
|
||||
|
||||
BorderPane.setAlignment(customField, Pos.CENTER_RIGHT);
|
||||
customField.disableProperty().bind(left.selectedProperty().not());
|
||||
|
||||
if (!customField.getValidators().isEmpty()) {
|
||||
FXUtils.setValidateWhileTextChanged(customField, true);
|
||||
}
|
||||
|
||||
pane.setRight(customField);
|
||||
|
||||
return pane;
|
||||
|
||||
@@ -25,7 +25,6 @@ import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -52,8 +51,8 @@ public class TwoLineListItem extends VBox {
|
||||
public TwoLineListItem() {
|
||||
setMouseTransparent(true);
|
||||
|
||||
FlowPane firstLine = new FlowPane();
|
||||
firstLine.setMaxWidth(Double.MAX_VALUE);
|
||||
HBox firstLine = new HBox();
|
||||
firstLine.getStyleClass().add("first-line");
|
||||
|
||||
Label lblTitle = new Label();
|
||||
lblTitle.getStyleClass().add("title");
|
||||
@@ -63,7 +62,7 @@ public class TwoLineListItem extends VBox {
|
||||
Label tagLabel = new Label();
|
||||
tagLabel.getStyleClass().add("tag");
|
||||
tagLabel.setText(tag);
|
||||
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
HBox.setMargin(tagLabel, new Insets(0, 8, 0, 0));
|
||||
return tagLabel;
|
||||
});
|
||||
firstLineChildren = new AggregatedObservableList<>();
|
||||
|
||||
@@ -24,11 +24,13 @@ import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
|
||||
@@ -43,6 +45,7 @@ import org.jackhuang.hmcl.ui.construct.Navigator;
|
||||
import org.jackhuang.hmcl.ui.construct.StackContainerPane;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardProvider;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -140,9 +143,15 @@ public class DecoratorController {
|
||||
.orElse(null);
|
||||
}
|
||||
if (config().getBackgroundImageType() == EnumBackgroundImage.NETWORK) {
|
||||
image = new Image(config().getBackgroundImageUrl(), true);
|
||||
if (!NetworkUtils.isURL(config().getBackgroundImageUrl())) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
} else {
|
||||
image = new Image(config().getBackgroundImageUrl(), true);
|
||||
}
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.CLASSIC) {
|
||||
image = newImage("/assets/img/background-classic.jpg");
|
||||
} else if (config().getBackgroundImageType() == EnumBackgroundImage.TRANSLUCENT) {
|
||||
return new Background(new BackgroundFill(new Color(1, 1, 1, 0.5), CornerRadii.EMPTY, Insets.EMPTY));
|
||||
}
|
||||
if (image == null) {
|
||||
image = loadDefaultBackgroundImage();
|
||||
|
||||
@@ -37,7 +37,6 @@ import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -56,7 +55,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
private final StackPane titleContainer;
|
||||
private final Stage primaryStage;
|
||||
private final TransitionPane navBarPane;
|
||||
private final StackPane leftPane;
|
||||
|
||||
private double xOffset, yOffset, newX, newY, initX, initY;
|
||||
private boolean titleBarTransparent = true;
|
||||
@@ -109,15 +107,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
StackPane container = new StackPane();
|
||||
FXUtils.setOverflowHidden(container);
|
||||
|
||||
// animation layer at bottom
|
||||
HBox drawerPane = new HBox();
|
||||
{
|
||||
leftPane = new StackPane();
|
||||
leftPane.setPrefWidth(0);
|
||||
leftPane.getStyleClass().add("jfx-decorator-drawer");
|
||||
drawerPane.getChildren().setAll(leftPane);
|
||||
}
|
||||
|
||||
// content layer at middle
|
||||
{
|
||||
StackPane contentPlaceHolder = new StackPane();
|
||||
@@ -159,16 +148,12 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
container.setBackground(null);
|
||||
titleContainer.getStyleClass().remove("background");
|
||||
titleContainer.getStyleClass().add("gray-background");
|
||||
container.getChildren().remove(drawerPane);
|
||||
wrapper.getChildren().add(0, drawerPane);
|
||||
} else {
|
||||
container.backgroundProperty().bind(skinnable.contentBackgroundProperty());
|
||||
wrapper.backgroundProperty().unbind();
|
||||
wrapper.setBackground(null);
|
||||
titleContainer.getStyleClass().add("background");
|
||||
titleContainer.getStyleClass().remove("gray-background");
|
||||
wrapper.getChildren().remove(drawerPane);
|
||||
container.getChildren().add(0, drawerPane);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -180,6 +165,7 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
Rectangle buttonsContainerPlaceHolder = new Rectangle();
|
||||
{
|
||||
navBarPane = new TransitionPane();
|
||||
navBarPane.setId("decoratorTitleTransitionPane");
|
||||
FXUtils.onChangeAndOperate(skinnable.stateProperty(), s -> {
|
||||
if (s == null) return;
|
||||
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
|
||||
@@ -197,9 +183,6 @@ public class DecoratorSkin extends SkinBase<Decorator> {
|
||||
} else {
|
||||
navBarPane.getChildren().setAll(node);
|
||||
}
|
||||
|
||||
FXUtils.playAnimation(leftPane, "animation",
|
||||
s.isAnimate() ? Duration.millis(160) : null, leftPane.prefWidthProperty(), null, s.getLeftPaneWidth(), FXUtils.SINE);
|
||||
});
|
||||
titleBar.setCenter(navBarPane);
|
||||
titleBar.setRight(buttonsContainerPlaceHolder);
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.jackhuang.hmcl.ui.construct.RipplerContainer;
|
||||
import org.jackhuang.hmcl.ui.wizard.Navigation;
|
||||
import org.jackhuang.hmcl.ui.wizard.Refreshable;
|
||||
import org.jackhuang.hmcl.ui.wizard.WizardPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
@@ -279,6 +280,6 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
@FXML
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/bmclapi_sponsor");
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.MicrosoftAuthenticationServer;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.HMCLAccounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -246,8 +246,8 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
? i18n("account.methods.microsoft.manual")
|
||||
: i18n("account.methods.microsoft.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && MicrosoftAuthenticationServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(MicrosoftAuthenticationServer.lastlyOpenedURL);
|
||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
||||
}
|
||||
});
|
||||
vbox.getChildren().setAll(hintPane);
|
||||
|
||||
@@ -102,10 +102,12 @@ public class PersonalizationPage extends StackPane {
|
||||
backgroundItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.default"), EnumBackgroundImage.DEFAULT),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.classic"), EnumBackgroundImage.CLASSIC),
|
||||
new MultiFileItem.Option<>(i18n("launcher.background.translucent"), EnumBackgroundImage.TRANSLUCENT),
|
||||
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
|
||||
.setChooserTitle(i18n("launcher.background.choose"))
|
||||
.bindBidirectional(config().backgroundImageProperty()),
|
||||
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
|
||||
.setValidators(new URLValidator(true))
|
||||
.bindBidirectional(config().backgroundImageUrlProperty())
|
||||
));
|
||||
backgroundItem.selectedDataProperty().bindBidirectional(config().backgroundImageTypeProperty());
|
||||
|
||||
@@ -87,12 +87,18 @@ public final class MultiplayerChannel {
|
||||
}
|
||||
|
||||
public static class JoinResponse extends Response {
|
||||
private final String sessionName;
|
||||
private final int port;
|
||||
|
||||
public JoinResponse(int port) {
|
||||
public JoinResponse(String sessionName, int port) {
|
||||
this.sessionName = sessionName;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
@@ -120,6 +126,10 @@ public final class MultiplayerChannel {
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static final String VERSION_NOT_MATCHED = "version_not_matched";
|
||||
public static final String KICKED = "kicked";
|
||||
public static final String JOIN_ACEEPTANCE_TIMEOUT = "join_acceptance_timeout";
|
||||
}
|
||||
|
||||
public static class CatoClient extends Event {
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.io.*;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.TimerTask;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -42,7 +43,7 @@ public class MultiplayerClient extends Thread {
|
||||
|
||||
private final EventManager<ConnectedEvent> onConnected = new EventManager<>();
|
||||
private final EventManager<Event> onDisconnected = new EventManager<>();
|
||||
private final EventManager<Event> onKicked = new EventManager<>();
|
||||
private final EventManager<KickEvent> onKicked = new EventManager<>();
|
||||
private final EventManager<Event> onHandshake = new EventManager<>();
|
||||
|
||||
public MultiplayerClient(String id, int port) {
|
||||
@@ -69,7 +70,7 @@ public class MultiplayerClient extends Thread {
|
||||
return onDisconnected;
|
||||
}
|
||||
|
||||
public EventManager<Event> onKicked() {
|
||||
public EventManager<KickEvent> onKicked() {
|
||||
return onKicked;
|
||||
}
|
||||
|
||||
@@ -83,8 +84,8 @@ public class MultiplayerClient extends Thread {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
KeepAliveThread keepAliveThread = null;
|
||||
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer);
|
||||
LOG.info("Connected to 127.0.0.1:" + port);
|
||||
|
||||
@@ -124,12 +125,12 @@ public class MultiplayerClient extends Thread {
|
||||
|
||||
connected = true;
|
||||
|
||||
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getPort()));
|
||||
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getSessionName(), joinResponse.getPort()));
|
||||
|
||||
LOG.fine("Received join response with port " + joinResponse.getPort());
|
||||
} else if (response instanceof KickResponse) {
|
||||
LOG.fine("Kicked by the server");
|
||||
onKicked.fireEvent(new Event(this));
|
||||
onKicked.fireEvent(new KickEvent(this, ((KickResponse) response).getMsg()));
|
||||
return;
|
||||
} else if (response instanceof KeepAliveResponse) {
|
||||
} else if (response instanceof HandshakeResponse) {
|
||||
@@ -192,15 +193,34 @@ public class MultiplayerClient extends Thread {
|
||||
}
|
||||
|
||||
public static class ConnectedEvent extends Event {
|
||||
private final String sessionName;
|
||||
private final int port;
|
||||
|
||||
public ConnectedEvent(Object source, int port) {
|
||||
public ConnectedEvent(Object source, String sessionName, int port) {
|
||||
super(source);
|
||||
this.sessionName = sessionName;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
||||
public static class KickEvent extends Event {
|
||||
private final String reason;
|
||||
|
||||
public KickEvent(Object source, String reason) {
|
||||
super(source);
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
@@ -35,7 +35,9 @@ import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
@@ -44,31 +46,64 @@ import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Lang.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/**
|
||||
* Cato Management.
|
||||
*/
|
||||
public final class MultiplayerManager {
|
||||
private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/";
|
||||
static final String CATO_VERSION = "1.0.c";
|
||||
static final String CATO_VERSION = "1.1.1-202110231414";
|
||||
// private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/cato/cato/" + MultiplayerManager.CATO_VERSION;
|
||||
private static final String CATO_DOWNLOAD_URL = "https://codechina.csdn.net/to/ioi_bin/-/raw/acb0524bcad82a31fa5a09bf4c79248ebd674de1/client/";
|
||||
private static final String CATO_PATH = getCatoPath();
|
||||
public static final int CATO_AGREEMENT_VERSION = 2;
|
||||
|
||||
private static final String REMOTE_ADDRESS = "127.0.0.1";
|
||||
private static final String LOCAL_ADDRESS = "0.0.0.0";
|
||||
|
||||
private static final Map<String, String> HASH = mapOf(
|
||||
pair("cato-client-darwin-amd64", "6b7630f843d4b8f8e11feff4c1cb3dc1916d1fdb"),
|
||||
pair("cato-client-darwin-arm64", "5f00153f0117eebb4209a4484152ec4077d34b34"),
|
||||
pair("cato-client-freebsd-amd64", "ac401f51e69058696e342ddaa62968d21c8252e8"),
|
||||
pair("cato-client-freebsd-arm7", "168dfdabc773ce87f0f1d901bee76f7ea3beacb3"),
|
||||
pair("cato-client-freebsd-arm64", "08daef0f3acc279411ff39e8a22510a9876d06cb"),
|
||||
pair("cato-client-freebsd-i386", "febdf99be30c671708dd80f8d3b48335a0d5920a"),
|
||||
pair("cato-client-js.wasm", "0439d2a1cd8ee854ef6057a8ae6898db47aba2bd"),
|
||||
pair("cato-client-linux-amd64", "816aecb116e2bc0727de9362e893bd9cefdf0485"),
|
||||
pair("cato-client-linux-arm7", "710a047fdf528917d8a45ed0161ca155e2e05ff1"),
|
||||
pair("cato-client-linux-arm64", "7a9a22d39f0ba200e1e4b2f6fca8cbe218a3eedf"),
|
||||
pair("cato-client-linux-i386", "3eb0b37bdff8b9c8dc1e425b91ddb4d0a72a0da0"),
|
||||
pair("cato-client-linux-mips", "8d2b383fcd4edb7903a14a93947ed5cb54838e60"),
|
||||
pair("cato-client-linux-mips64", "248f1a3fe69ec97f6c095b63fa597ee099b4cb8c"),
|
||||
pair("cato-client-linux-mips64le", "8d2f8d93ca582ab5f43dc4570a0d5b18dbe06df3"),
|
||||
pair("cato-client-linux-mipsle", "f38bd14e6cdd6bfcd045ccd5a0a183bc8083c028"),
|
||||
pair("cato-client-linux-ppc64", "db420847ed8e60a58a69b2e1cb55cf21b7b56e4b"),
|
||||
pair("cato-client-linux-ppc64le", "717149f52a0808ee09cd5a7e89f8b9a6ed604cb0"),
|
||||
pair("cato-client-openbsd-amd64", "248aaf3ca3bcaa18d4d1391325d171ef17e65244"),
|
||||
pair("cato-client-openbsd-arm7", "1ab97264dba5d2b61388ee68e49f890c29c4e09f"),
|
||||
pair("cato-client-openbsd-arm64", "5f28541ace9d298b816eb43963be8e4cd87bd0fd"),
|
||||
pair("cato-client-openbsd-i386", "613b5c3fc0382815843a6e9b2a5281b05967bc1c"),
|
||||
pair("cato-client-windows-amd64.exe", "64f19648d281882eefc5afaa3af347907af5cb23"),
|
||||
pair("cato-client-windows-arm64.exe", "48a4bc5e18c35c93c9a3ecc500420d3abb5ae6f0"),
|
||||
pair("cato-client-windows-i386.exe", "fcb4f245da2e293badb73fac04cf4e83fbd79b18")
|
||||
|
||||
);
|
||||
|
||||
private MultiplayerManager() {
|
||||
}
|
||||
|
||||
public static Task<Void> downloadCato() {
|
||||
return new FileDownloadTask(
|
||||
NetworkUtils.toURL(CATO_DOWNLOAD_URL + CATO_PATH),
|
||||
getCatoExecutable().toFile()
|
||||
NetworkUtils.toURL(CATO_DOWNLOAD_URL + getCatoFileName()),
|
||||
getCatoExecutable().toFile(),
|
||||
new FileDownloadTask.IntegrityCheck("SHA-1", HASH.get(getCatoFileName()))
|
||||
).thenRunAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(getCatoExecutable());
|
||||
@@ -82,14 +117,8 @@ public final class MultiplayerManager {
|
||||
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
|
||||
if (!CATO_VERSION.equals(version)) {
|
||||
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
||||
}
|
||||
|
||||
LOG.info(String.format("Joining session (token=%s,version=%s,sessionName=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, version, sessionName, peer, mode, remotePort, localPort));
|
||||
|
||||
return CompletableFuture.completedFuture(null).thenComposeAsync(unused -> {
|
||||
private static CompletableFuture<CatoSession> startCato(String token, State state) {
|
||||
return CompletableFuture.completedFuture(null).thenApplyAsync(wrap(unused -> {
|
||||
Path exe = getCatoExecutable();
|
||||
if (!Files.isRegularFile(exe)) {
|
||||
throw new CatoNotExistsException(exe);
|
||||
@@ -99,36 +128,51 @@ public final class MultiplayerManager {
|
||||
throw new CatoAlreadyStartedException();
|
||||
}
|
||||
|
||||
String[] commands = new String[]{exe.toString(),
|
||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||
"--id", peer,
|
||||
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
||||
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort),
|
||||
"--mode", mode.getName()};
|
||||
Process process;
|
||||
try {
|
||||
process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
ChecksumMismatchException.verifyChecksum(exe, "SHA-1", HASH.get(getCatoFileName()));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
Files.deleteIfExists(exe);
|
||||
throw e;
|
||||
}
|
||||
|
||||
CatoSession session = new CatoSession(sessionName, State.SLAVE, process, Arrays.asList(commands));
|
||||
String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token};
|
||||
Process process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
|
||||
return new CatoSession(state, process, Arrays.asList(commands));
|
||||
}));
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> joinSession(String token, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
|
||||
LOG.info(String.format("Joining session (token=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, peer, mode, remotePort, localPort));
|
||||
|
||||
return startCato(token, State.SLAVE).thenComposeAsync(wrap(session -> {
|
||||
CompletableFuture<CatoSession> future = new CompletableFuture<>();
|
||||
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream()));
|
||||
session.forwardPort(peer, LOCAL_ADDRESS, localPort, REMOTE_ADDRESS, remotePort, mode);
|
||||
|
||||
session.onExit().register(() -> {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to close cato session stdin writer", e);
|
||||
Consumer<CatoExitEvent> onExit = event -> {
|
||||
boolean ready = session.isReady();
|
||||
switch (event.getExitCode()) {
|
||||
case 1:
|
||||
if (!ready) {
|
||||
future.completeExceptionally(new CatoExitTimeoutException());
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
|
||||
};
|
||||
session.onExit.register(onExit);
|
||||
|
||||
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
|
||||
future.completeExceptionally(new PeerConnectionTimeoutException());
|
||||
session.stop();
|
||||
}, 15 * 1000);
|
||||
|
||||
session.onPeerConnected.register(event -> {
|
||||
peerConnectionTimeoutTask.cancel();
|
||||
|
||||
MultiplayerClient client = new MultiplayerClient(session.getId(), localPort);
|
||||
session.addRelatedThread(client);
|
||||
session.setClient(client);
|
||||
@@ -141,13 +185,11 @@ public final class MultiplayerManager {
|
||||
client.onConnected().register(connectedEvent -> {
|
||||
try {
|
||||
int port = findAvailablePort();
|
||||
String command = String.format("net add %s %s:%d %s:%d %s", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode.getName());
|
||||
LOG.info("Invoking cato: " + command);
|
||||
session.forwardPort(peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode);
|
||||
session.addRelatedThread(Lang.thread(new LocalServerBroadcaster(port, session), "LocalServerBroadcaster", true));
|
||||
session.setName(connectedEvent.getSessionName());
|
||||
client.setGamePort(port);
|
||||
writer.write(command);
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
session.onExit.unregister(onExit);
|
||||
future.complete(session);
|
||||
} catch (IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
@@ -175,40 +217,59 @@ public final class MultiplayerManager {
|
||||
});
|
||||
|
||||
return future;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public static CatoSession createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) throws IOException {
|
||||
Path exe = getCatoExecutable();
|
||||
if (!Files.isRegularFile(exe)) {
|
||||
throw new FileNotFoundException("Cato file not found");
|
||||
}
|
||||
|
||||
if (!isPortAvailable(3478)) {
|
||||
throw new CatoAlreadyStartedException();
|
||||
}
|
||||
|
||||
public static CompletableFuture<CatoSession> createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) {
|
||||
LOG.info(String.format("Creating session (token=%s,sessionName=%s,gamePort=%d)", token, sessionName, gamePort));
|
||||
|
||||
MultiplayerServer server = new MultiplayerServer(gamePort, allowAllJoinRequests);
|
||||
server.startServer();
|
||||
return startCato(token, State.MASTER).thenComposeAsync(wrap(session -> {
|
||||
CompletableFuture<CatoSession> future = new CompletableFuture<>();
|
||||
|
||||
String[] commands = new String[]{exe.toString(),
|
||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)};
|
||||
Process process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
|
||||
server.startServer();
|
||||
|
||||
CatoSession session = new CatoSession(sessionName, State.MASTER, process, Arrays.asList(commands));
|
||||
session.setServer(server);
|
||||
session.addRelatedThread(server);
|
||||
return session;
|
||||
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
|
||||
session.allowForwardingAddress(REMOTE_ADDRESS, gamePort);
|
||||
session.showAllowedAddress();
|
||||
|
||||
Consumer<CatoExitEvent> onExit = event -> {
|
||||
boolean ready = session.isReady();
|
||||
switch (event.getExitCode()) {
|
||||
case 1:
|
||||
if (!ready) {
|
||||
future.completeExceptionally(new CatoExitTimeoutException());
|
||||
}
|
||||
break;
|
||||
}
|
||||
future.completeExceptionally(new CatoExitException(event.getExitCode(), ready));
|
||||
};
|
||||
|
||||
session.onExit.register(onExit);
|
||||
session.setServer(server);
|
||||
session.addRelatedThread(server);
|
||||
|
||||
TimerTask peerConnectionTimeoutTask = Lang.setTimeout(() -> {
|
||||
future.completeExceptionally(new PeerConnectionTimeoutException());
|
||||
session.stop();
|
||||
}, 15 * 1000);
|
||||
|
||||
session.onPeerConnected.register(event -> {
|
||||
session.onExit.unregister(onExit);
|
||||
future.complete(session);
|
||||
peerConnectionTimeoutTask.cancel();
|
||||
});
|
||||
|
||||
return future;
|
||||
}));
|
||||
}
|
||||
|
||||
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>(idx|mix)(.*?))#(?<port>\\d{2,5})$");
|
||||
|
||||
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
|
||||
String json = new String(Base64.getDecoder().decode(invitationCode.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
return JsonUtils.fromNonNullJson(json, Invitation.class);
|
||||
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
|
||||
if (!matcher.find()) throw new IllegalArgumentException("Invalid invitation code");
|
||||
return new Invitation(matcher.group("id"), Integer.parseInt(matcher.group("port")));
|
||||
}
|
||||
|
||||
public static int findAvailablePort() throws IOException {
|
||||
@@ -225,27 +286,33 @@ public final class MultiplayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCatoPath() {
|
||||
private static String getCatoFileName() {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-windows-amd64.exe";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-windows-amd64.exe";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-windows-arm64.exe";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return "cato-client-windows-i386.exe";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case OSX:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-darwin-amd64";
|
||||
} else if (Architecture.SYSTEM == Architecture.ARM64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-darwin-arm64";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-darwin-amd64";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-darwin-arm64";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
case LINUX:
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-amd64";
|
||||
} else if (Architecture.SYSTEM == Architecture.ARM || Architecture.SYSTEM == Architecture.ARM64) {
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-arm7";
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return "cato-client-linux-amd64";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM32) {
|
||||
return "cato-client-linux-arm7";
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return "cato-client-linux-arm64";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@@ -254,30 +321,38 @@ public final class MultiplayerManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static String getCatoPath() {
|
||||
String name = getCatoFileName();
|
||||
if (StringUtils.isBlank(name)) return "";
|
||||
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/" + name;
|
||||
}
|
||||
|
||||
public static class CatoSession extends ManagedProcess {
|
||||
private final EventManager<CatoExitEvent> onExit = new EventManager<>();
|
||||
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();
|
||||
private final EventManager<Event> onPeerConnected = new EventManager<>();
|
||||
|
||||
private final String name;
|
||||
private String name;
|
||||
private final State type;
|
||||
private String id;
|
||||
private boolean peerConnected = false;
|
||||
private MultiplayerClient client;
|
||||
private MultiplayerServer server;
|
||||
private final BufferedWriter writer;
|
||||
|
||||
CatoSession(String name, State type, Process process, List<String> commands) {
|
||||
CatoSession(State type, Process process, List<String> commands) {
|
||||
super(process, commands);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
|
||||
|
||||
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands).toString());
|
||||
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands));
|
||||
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
addRelatedThread(Lang.thread(this::waitFor, "CatoExitWaiter", true));
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), this::checkCatoLog), "CatoInputStreamPump", true));
|
||||
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), this::checkCatoLog), "CatoErrorStreamPump", true));
|
||||
|
||||
writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
public MultiplayerClient getClient() {
|
||||
@@ -324,6 +399,12 @@ public final class MultiplayerManager {
|
||||
onExit.fireEvent(new CatoExitEvent(this, exitCode));
|
||||
} catch (InterruptedException e) {
|
||||
onExit.fireEvent(new CatoExitEvent(this, CatoExitEvent.EXIT_CODE_INTERRUPTED));
|
||||
} finally {
|
||||
try {
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to close cato stdin writer", e);
|
||||
}
|
||||
}
|
||||
destroyRelatedThreads();
|
||||
}
|
||||
@@ -336,6 +417,10 @@ public final class MultiplayerManager {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public State getType() {
|
||||
return type;
|
||||
}
|
||||
@@ -349,8 +434,26 @@ public final class MultiplayerManager {
|
||||
if (id == null) {
|
||||
throw new IllegalStateException("id not generated");
|
||||
}
|
||||
String json = JsonUtils.GSON.toJson(new Invitation(CATO_VERSION, id, name, serverPort));
|
||||
return new String(Base64.getEncoder().encode(json.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
|
||||
return id + "#" + serverPort;
|
||||
}
|
||||
|
||||
public synchronized void invokeCommand(String command) throws IOException {
|
||||
LOG.info("Invoking cato: " + command);
|
||||
writer.write(command);
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
public void forwardPort(String peerId, String localAddress, int localPort, String remoteAddress, int remotePort, Mode mode) throws IOException {
|
||||
invokeCommand(String.format("net add %s %s:%d %s:%d %s", peerId, localAddress, localPort, remoteAddress, remotePort, mode.getName()));
|
||||
}
|
||||
|
||||
public void allowForwardingAddress(String address, int port) throws IOException {
|
||||
invokeCommand(String.format("ufw net open %s:%d", address, port));
|
||||
}
|
||||
|
||||
public void showAllowedAddress() throws IOException {
|
||||
invokeCommand("ufw net whitelist");
|
||||
}
|
||||
|
||||
public EventManager<CatoExitEvent> onExit() {
|
||||
@@ -407,33 +510,19 @@ public final class MultiplayerManager {
|
||||
}
|
||||
|
||||
public static class Invitation {
|
||||
@SerializedName("v")
|
||||
private final String version;
|
||||
private final String id;
|
||||
@SerializedName("n")
|
||||
private final String sessionName;
|
||||
@SerializedName("p")
|
||||
private final int channelPort;
|
||||
|
||||
public Invitation(String version, String id, String sessionName, int channelPort) {
|
||||
this.version = version;
|
||||
public Invitation(String id, int channelPort) {
|
||||
this.id = id;
|
||||
this.sessionName = sessionName;
|
||||
this.channelPort = channelPort;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getSessionName() {
|
||||
return sessionName;
|
||||
}
|
||||
|
||||
public int getChannelPort() {
|
||||
return channelPort;
|
||||
}
|
||||
@@ -463,22 +552,49 @@ public final class MultiplayerManager {
|
||||
|
||||
public enum Mode {
|
||||
P2P,
|
||||
RELAY;
|
||||
BRIDGE;
|
||||
|
||||
String getName() {
|
||||
return name().toLowerCase(Locale.ROOT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatoExitException extends RuntimeException {
|
||||
private final int exitCode;
|
||||
private final boolean ready;
|
||||
|
||||
public CatoExitException(int exitCode, boolean ready) {
|
||||
this.exitCode = exitCode;
|
||||
this.ready = ready;
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatoExitTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class CatoSessionExpiredException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class CatoAlreadyStartedException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class JoinRequestTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class PeerConnectionTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class ConnectionErrorException extends RuntimeException {
|
||||
}
|
||||
|
||||
|
||||
public static class CatoNotExistsException extends RuntimeException {
|
||||
private final Path file;
|
||||
|
||||
|
||||
@@ -37,15 +37,18 @@ import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.KickResponse.*;
|
||||
import static org.jackhuang.hmcl.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -146,7 +149,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
agreementPane.setHeading(new Label(i18n("launcher.agreement")));
|
||||
agreementPane.setBody(new Label(i18n("multiplayer.agreement.prompt")));
|
||||
JFXHyperlink agreementLink = new JFXHyperlink(i18n("launcher.agreement"));
|
||||
agreementLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/agreement"));
|
||||
agreementLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-agreement"));
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
@@ -213,48 +216,36 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
|
||||
int gamePort = result.getServer().getAd();
|
||||
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
|
||||
try {
|
||||
MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests());
|
||||
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
||||
runInFX(() -> {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
|
||||
.yesOrNo(resolveClient, () -> rejectClient.accept(i18n("multiplayer.session.join.wait_timeout")))
|
||||
.cancelOnTimeout(30 * 1000)
|
||||
.build());
|
||||
});
|
||||
});
|
||||
session.getServer().onClientAdded().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.add(event);
|
||||
});
|
||||
});
|
||||
session.getServer().onClientDisconnected().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.remove(event);
|
||||
});
|
||||
});
|
||||
initCatoSession(session);
|
||||
} catch (MultiplayerManager.CatoAlreadyStartedException e) {
|
||||
LOG.log(Level.WARNING, "Cato already started", e);
|
||||
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||
return;
|
||||
} catch (MultiplayerManager.CatoNotExistsException e) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + e.getFile(), e);
|
||||
reject.accept(i18n("multiplayer.session.error.file_not_found"));
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LOG.log(Level.WARNING, "Failed to create session", e);
|
||||
if (isStaticToken) {
|
||||
reject.accept(i18n("multiplayer.session.create.error.static_token") + e.getLocalizedMessage());
|
||||
} else {
|
||||
reject.accept(i18n("multiplayer.session.create.error.dynamic_token") + e.getLocalizedMessage());
|
||||
}
|
||||
return;
|
||||
}
|
||||
MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests())
|
||||
.thenAcceptAsync(session -> {
|
||||
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
||||
runInFX(() -> {
|
||||
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
|
||||
.yesOrNo(resolveClient, () -> rejectClient.accept(MultiplayerChannel.KickResponse.JOIN_ACEEPTANCE_TIMEOUT))
|
||||
.cancelOnTimeout(30 * 1000)
|
||||
.build());
|
||||
});
|
||||
});
|
||||
session.getServer().onClientAdded().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.add(event);
|
||||
});
|
||||
});
|
||||
session.getServer().onClientDisconnected().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.remove(event);
|
||||
});
|
||||
});
|
||||
initCatoSession(session);
|
||||
|
||||
this.gamePort.set(gamePort);
|
||||
setMultiplayerState(MultiplayerManager.State.CONNECTING);
|
||||
resolve.run();
|
||||
this.gamePort.set(gamePort);
|
||||
setMultiplayerState(MultiplayerManager.State.MASTER);
|
||||
resolve.run();
|
||||
}, Platform::runLater)
|
||||
.exceptionally(throwable -> {
|
||||
reject.accept(localizeCreateErrorMessage(throwable, isStaticToken));
|
||||
return null;
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -265,6 +256,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
|
||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> {
|
||||
PromptDialogPane.Builder.HintQuestion hintQuestion = (PromptDialogPane.Builder.HintQuestion) result.get(0);
|
||||
boolean isStaticToken = StringUtils.isNotBlank(globalConfig().getMultiplayerToken());
|
||||
|
||||
String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue();
|
||||
MultiplayerManager.Invitation invitation;
|
||||
@@ -287,11 +279,9 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
try {
|
||||
MultiplayerManager.joinSession(
|
||||
globalConfig().getMultiplayerToken(),
|
||||
invitation.getVersion(),
|
||||
invitation.getSessionName(),
|
||||
invitation.getId(),
|
||||
globalConfig().isMultiplayerRelay() && StringUtils.isNotBlank(globalConfig().getMultiplayerToken())
|
||||
? MultiplayerManager.Mode.RELAY
|
||||
globalConfig().isMultiplayerRelay() && (StringUtils.isNotBlank(globalConfig().getMultiplayerToken()) || StringUtils.isNotBlank(System.getProperty("hmcl.multiplayer.relay")))
|
||||
? MultiplayerManager.Mode.BRIDGE
|
||||
: MultiplayerManager.Mode.P2P,
|
||||
invitation.getChannelPort(),
|
||||
localPort, new MultiplayerManager.JoinSessionHandler() {
|
||||
@@ -316,10 +306,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
});
|
||||
});
|
||||
|
||||
session.getClient().onKicked().register(() -> {
|
||||
session.getClient().onKicked().register(kickedEvent -> {
|
||||
runInFX(() -> {
|
||||
kicked.set(true);
|
||||
Controllers.dialog(i18n("multiplayer.session.join.kicked"));
|
||||
Controllers.dialog(i18n("multiplayer.session.join.kicked", localizeKickMessage(kickedEvent.getReason())));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -328,31 +318,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
resolve.run();
|
||||
}, Platform::runLater)
|
||||
.exceptionally(throwable -> {
|
||||
Throwable resolved = resolveException(throwable);
|
||||
if (resolved instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
reject.accept(i18n("multiplayer.session.join.rejected"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.CatoAlreadyStartedException) {
|
||||
LOG.info("Cato already started");
|
||||
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||
return null;
|
||||
} else if (throwable instanceof MultiplayerManager.CatoNotExistsException) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + ((MultiplayerManager.CatoNotExistsException) throwable).getFile(), throwable);
|
||||
reject.accept(i18n("multiplayer.session.error.file_not_found"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.JoinRequestTimeoutException) {
|
||||
LOG.info("Cato already started");
|
||||
reject.accept(i18n("multiplayer.session.join.wait_timeout"));
|
||||
return null;
|
||||
} else if (resolved instanceof MultiplayerManager.ConnectionErrorException) {
|
||||
LOG.info("Failed to establish connection with server");
|
||||
reject.accept(i18n("multiplayer.session.join.error.connection"));
|
||||
return null;
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to join session", resolved);
|
||||
reject.accept(i18n("multiplayer.session.join.error"));
|
||||
}
|
||||
reject.accept(localizeJoinErrorMessage(throwable, isStaticToken));
|
||||
return null;
|
||||
});
|
||||
} catch (MultiplayerManager.IncompatibleCatoVersionException e) {
|
||||
@@ -363,6 +329,72 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator())));
|
||||
}
|
||||
|
||||
private String localizeKickMessage(String message) {
|
||||
if (VERSION_NOT_MATCHED.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.version_not_matched");
|
||||
} else if (KICKED.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.kicked");
|
||||
} else if (JOIN_ACEEPTANCE_TIMEOUT.equals(message)) {
|
||||
return i18n("multiplayer.session.join.kicked.join_acceptance_timeout");
|
||||
} else {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
private String localizeErrorMessage(Throwable t, boolean isStaticToken, Function<Throwable, String> fallback) {
|
||||
Throwable e = resolveException(t);
|
||||
if (e instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
return i18n("multiplayer.session.join.rejected");
|
||||
} else if (e instanceof MultiplayerManager.CatoAlreadyStartedException) {
|
||||
LOG.info("Cato already started");
|
||||
return i18n("multiplayer.session.error.already_started");
|
||||
} else if (e instanceof MultiplayerManager.CatoNotExistsException) {
|
||||
LOG.log(Level.WARNING, "Cato not found " + ((MultiplayerManager.CatoNotExistsException) e).getFile(), e);
|
||||
return i18n("multiplayer.session.error.file_not_found");
|
||||
} else if (e instanceof MultiplayerManager.JoinRequestTimeoutException) {
|
||||
LOG.info("Cato already started");
|
||||
return i18n("multiplayer.session.join.wait_timeout");
|
||||
} else if (e instanceof MultiplayerManager.ConnectionErrorException) {
|
||||
LOG.info("Failed to establish connection with server");
|
||||
return i18n("multiplayer.session.join.error.connection");
|
||||
} else if (e instanceof MultiplayerManager.CatoExitTimeoutException) {
|
||||
LOG.info("Cato failed to connect to main net");
|
||||
if (isStaticToken) {
|
||||
return i18n("multiplayer.exit.timeout.static_token");
|
||||
} else {
|
||||
return i18n("multiplayer.exit.timeout.dynamic_token");
|
||||
}
|
||||
} else if (e instanceof MultiplayerManager.CatoExitException) {
|
||||
LOG.info("Cato exited accidentally");
|
||||
if (!((MultiplayerManager.CatoExitException) e).isReady()) {
|
||||
return i18n("multiplayer.exit.before_ready");
|
||||
} else {
|
||||
return i18n("multiplayer.exit.after_ready");
|
||||
}
|
||||
} else {
|
||||
return fallback.apply(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String localizeCreateErrorMessage(Throwable t, boolean isStaticToken) {
|
||||
return localizeErrorMessage(t, isStaticToken, e -> {
|
||||
LOG.log(Level.WARNING, "Failed to create session", e);
|
||||
if (isStaticToken) {
|
||||
return i18n("multiplayer.session.create.error.static_token") + e.getLocalizedMessage();
|
||||
} else {
|
||||
return i18n("multiplayer.session.create.error.dynamic_token") + e.getLocalizedMessage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String localizeJoinErrorMessage(Throwable t, boolean isStaticToken) {
|
||||
return localizeErrorMessage(t, isStaticToken, e -> {
|
||||
LOG.log(Level.WARNING, "Failed to join session", e);
|
||||
return i18n("multiplayer.session.join.error");
|
||||
});
|
||||
}
|
||||
|
||||
public void kickPlayer(MultiplayerChannel.CatoClient client) {
|
||||
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
|
||||
throw new IllegalStateException("CatoSession not ready");
|
||||
|
||||
@@ -28,7 +28,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
@@ -41,6 +40,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
@@ -130,9 +130,9 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
item.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/help/launcher/multiplayer.html"));
|
||||
})
|
||||
.addNavigationDrawerItem(report -> {
|
||||
report.setTitle(i18n("multiplayer.report"));
|
||||
report.setLeftGraphic(wrap(SVG::bug));
|
||||
report.setOnAction(e -> FXUtils.openLink(Metadata.EULA_URL));
|
||||
report.setTitle(i18n("feedback"));
|
||||
report.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||
report.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-feedback"));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -256,7 +256,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
tokenField.setPromptText(i18n("multiplayer.session.create.token.prompt"));
|
||||
|
||||
JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.session.create.token.apply"));
|
||||
applyLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/circle/386.html"));
|
||||
applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token"));
|
||||
|
||||
gridPane.addRow(0, new Label(i18n("multiplayer.session.create.token")), tokenField, applyLink);
|
||||
|
||||
@@ -270,7 +270,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXHyperlink aboutLink = new JFXHyperlink(i18n("about"));
|
||||
aboutLink.setOnAction(e -> FXUtils.openLink("https://noin.cn/71.html"));
|
||||
aboutLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-about"));
|
||||
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
|
||||
@@ -27,7 +27,9 @@ import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import java.io.*;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -36,6 +38,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
|
||||
public class MultiplayerServer extends Thread {
|
||||
private ServerSocket socket;
|
||||
private final String sessionName;
|
||||
private final int gamePort;
|
||||
private final boolean allowAllJoinRequests;
|
||||
|
||||
@@ -48,7 +51,8 @@ public class MultiplayerServer extends Thread {
|
||||
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
|
||||
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
||||
|
||||
public MultiplayerServer(int gamePort, boolean allowAllJoinRequests) {
|
||||
public MultiplayerServer(String sessionName, int gamePort, boolean allowAllJoinRequests) {
|
||||
this.sessionName = sessionName;
|
||||
this.gamePort = gamePort;
|
||||
this.allowAllJoinRequests = allowAllJoinRequests;
|
||||
|
||||
@@ -116,7 +120,7 @@ public class MultiplayerServer extends Thread {
|
||||
|
||||
try {
|
||||
if (client.socket.isConnected()) {
|
||||
client.write(new KickResponse(""));
|
||||
client.write(new KickResponse(KickResponse.KICKED));
|
||||
client.socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
@@ -129,8 +133,8 @@ public class MultiplayerServer extends Thread {
|
||||
String clientName = null;
|
||||
LOG.info("Accepted client " + address);
|
||||
try (Socket clientSocket = targetSocket;
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream(), StandardCharsets.UTF_8))) {
|
||||
clientSocket.setKeepAlive(true);
|
||||
Endpoint endpoint = new Endpoint(clientSocket, writer);
|
||||
clients.put(address, endpoint);
|
||||
@@ -149,6 +153,17 @@ public class MultiplayerServer extends Thread {
|
||||
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
|
||||
clientName = joinRequest.getUsername();
|
||||
|
||||
if (!Objects.equals(MultiplayerManager.CATO_VERSION, joinRequest.getClientVersion())) {
|
||||
try {
|
||||
endpoint.write(new KickResponse(KickResponse.VERSION_NOT_MATCHED));
|
||||
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send kick response.", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CatoClient catoClient = new CatoClient(this, clientName);
|
||||
nameClientMap.put(clientName, endpoint);
|
||||
onClientAdded.fireEvent(catoClient);
|
||||
@@ -156,7 +171,7 @@ public class MultiplayerServer extends Thread {
|
||||
if (onClientAdding != null && !allowAllJoinRequests) {
|
||||
onClientAdding.call(catoClient, () -> {
|
||||
try {
|
||||
endpoint.write(new JoinResponse(gamePort));
|
||||
endpoint.write(new JoinResponse(sessionName, gamePort));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send join response.", e);
|
||||
try {
|
||||
@@ -177,7 +192,7 @@ public class MultiplayerServer extends Thread {
|
||||
});
|
||||
} else {
|
||||
// Allow all join requests.
|
||||
endpoint.write(new JoinResponse(gamePort));
|
||||
endpoint.write(new JoinResponse(sessionName, gamePort));
|
||||
}
|
||||
} else if (request instanceof KeepAliveRequest) {
|
||||
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));
|
||||
|
||||
@@ -119,7 +119,9 @@ public final class ModTranslations {
|
||||
modIdMap = new HashMap<>();
|
||||
for (Mod mod : mods) {
|
||||
for (String id : mod.getModIds()) {
|
||||
modIdMap.put(id, mod);
|
||||
if (StringUtils.isNotBlank(id) && !"examplemod".equals(id)) {
|
||||
modIdMap.put(id, mod);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -45,9 +45,11 @@ import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
@@ -65,6 +67,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.newImage;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public final class VersionSettingsPage extends StackPane implements DecoratorPage, VersionPage.VersionLoadable, PageAware {
|
||||
@@ -98,9 +101,9 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
private final OptionToggleButton useNativeGLFWPane;
|
||||
private final OptionToggleButton useNativeOpenALPane;
|
||||
private final ComponentSublist javaSublist;
|
||||
private final MultiFileItem<JavaVersion> javaItem;
|
||||
private final MultiFileItem.Option<JavaVersion> javaAutoDeterminedOption;
|
||||
private final MultiFileItem.FileOption<JavaVersion> javaCustomOption;
|
||||
private final MultiFileItem<Pair<JavaVersionType, JavaVersion>> javaItem;
|
||||
private final MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>> javaAutoDeterminedOption;
|
||||
private final MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>> javaCustomOption;
|
||||
private final ComponentSublist gameDirSublist;
|
||||
private final MultiFileItem<GameDirectoryType> gameDirItem;
|
||||
private final MultiFileItem.FileOption<GameDirectoryType> gameDirCustomOption;
|
||||
@@ -115,7 +118,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
private final InvalidationListener javaListener = any -> initJavaSubtitle();
|
||||
|
||||
private boolean uiVisible = false;
|
||||
private final StringProperty selectedVersion = new SimpleStringProperty();
|
||||
private final BooleanProperty navigateToSpecificSettings = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty enableSpecificSettings = new SimpleBooleanProperty(true);
|
||||
@@ -200,8 +202,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
javaSublist.getContent().add(javaItem);
|
||||
javaSublist.setTitle(i18n("settings.game.java_directory"));
|
||||
javaSublist.setHasSubtitle(true);
|
||||
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), null);
|
||||
javaCustomOption = new MultiFileItem.FileOption<JavaVersion>(i18n("settings.custom"), null)
|
||||
javaAutoDeterminedOption = new MultiFileItem.Option<>(i18n("settings.game.java_directory.auto"), pair(JavaVersionType.AUTO, null));
|
||||
javaCustomOption = new MultiFileItem.FileOption<Pair<JavaVersionType, JavaVersion>>(i18n("settings.custom"), pair(JavaVersionType.CUSTOM, null))
|
||||
.setChooserTitle(i18n("settings.game.java_directory.choose"));
|
||||
|
||||
gameDirItem = new MultiFileItem<>();
|
||||
@@ -211,8 +213,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirSublist.setHasSubtitle(true);
|
||||
gameDirItem.disableProperty().bind(modpack);
|
||||
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
.setChooserTitle(i18n("settings.game.working_directory.choose"))
|
||||
.setDirectory(true);
|
||||
|
||||
gameDirItem.loadChildren(Arrays.asList(
|
||||
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
|
||||
@@ -399,7 +401,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
txtServerIP = new JFXTextField();
|
||||
txtServerIP.setPromptText(i18n("settings.advanced.server_ip.prompt"));
|
||||
txtServerIP.getStyleClass().add("fit-width");
|
||||
FXUtils.setLimitWidth(txtServerIP, 300);
|
||||
serverPane.addRow(0, new Label(i18n("settings.advanced.server_ip")), txtServerIP);
|
||||
}
|
||||
@@ -545,9 +546,15 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
memoryStatus.set(OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID));
|
||||
|
||||
Task.supplyAsync(JavaVersion::getJavas).thenAcceptAsync(Schedulers.javafx(), list -> {
|
||||
List<MultiFileItem.Option<JavaVersion>> options = list.stream()
|
||||
.map(javaVersion -> new MultiFileItem.Option<>(javaVersion.getVersion() + i18n("settings.game.java_directory.bit",
|
||||
javaVersion.getPlatform().getBit()), javaVersion)
|
||||
boolean isX86 = Architecture.SYSTEM_ARCH.isX86() && list.stream().allMatch(java -> java.getArchitecture().isX86());
|
||||
|
||||
List<MultiFileItem.Option<Pair<JavaVersionType, JavaVersion>>> options = list.stream()
|
||||
.map(javaVersion -> new MultiFileItem.Option<>(
|
||||
i18n("settings.game.java_directory.template",
|
||||
javaVersion.getVersion(),
|
||||
isX86 ? i18n("settings.game.java_directory.bit", javaVersion.getBits().getBit())
|
||||
: javaVersion.getPlatform().getArchitecture().getDisplayName()),
|
||||
pair(JavaVersionType.DETECTED, javaVersion))
|
||||
.setSubtitle(javaVersion.getBinary().toString()))
|
||||
.collect(Collectors.toList());
|
||||
options.add(0, javaAutoDeterminedOption);
|
||||
@@ -558,7 +565,6 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
}).start();
|
||||
|
||||
javaItem.setSelectedData(null);
|
||||
javaItem.setFallbackData(JavaVersion.fromCurrentEnvironment());
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
javaCustomOption.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java", "java.exe", "javaw.exe"));
|
||||
|
||||
@@ -684,7 +690,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
} else if (javaAutoDeterminedOption.isSelected()) {
|
||||
versionSetting.setJavaAutoSelected();
|
||||
} else {
|
||||
versionSetting.setJavaVersion((JavaVersion) newValue.getUserData());
|
||||
//noinspection unchecked
|
||||
versionSetting.setJavaVersion(((Pair<JavaVersionType, JavaVersion>) newValue.getUserData()).getValue());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -695,7 +702,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
gameDirItem.selectedDataProperty().bindBidirectional(versionSetting.gameDirTypeProperty());
|
||||
gameDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath()).normalize().toString(),
|
||||
versionSetting.gameDirProperty(), versionSetting.gameDirTypeProperty()));
|
||||
|
||||
|
||||
nativesDirItem.selectedDataProperty().bindBidirectional(versionSetting.nativesDirTypeProperty());
|
||||
nativesDirSublist.subtitleProperty().bind(Bindings.createStringBinding(() -> Paths.get(profile.getRepository().getRunDirectory(versionId).getAbsolutePath() + "/natives").normalize().toString(),
|
||||
versionSetting.nativesDirProperty(), versionSetting.nativesDirTypeProperty()));
|
||||
@@ -721,7 +728,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
// javaLoading.set(true);
|
||||
lastVersionSetting.getJavaVersion(null, null)
|
||||
.thenAcceptAsync(Schedulers.javafx(), javaVersion -> {
|
||||
javaItem.setSelectedData(javaVersion);
|
||||
javaItem.setSelectedData(pair(JavaVersionType.DETECTED, javaVersion));
|
||||
// javaLoading.set(false);
|
||||
}).start();
|
||||
}
|
||||
@@ -751,7 +758,8 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
profile.getRepository().getVersion(versionId));
|
||||
}
|
||||
}).thenAcceptAsync(Schedulers.javafx(), javaVersion -> javaSublist.setSubtitle(Optional.ofNullable(javaVersion)
|
||||
.map(JavaVersion::getBinary).map(Path::toString).orElse("Invalid Java Path")))
|
||||
.map(JavaVersion::getBinary).map(Path::toString).orElseGet(() ->
|
||||
autoSelected ? i18n("settings.game.java_directory.auto.not_found") : i18n("settings.game.java_directory.invalid"))))
|
||||
.start();
|
||||
}
|
||||
|
||||
@@ -808,4 +816,10 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private enum JavaVersionType {
|
||||
DETECTED,
|
||||
CUSTOM,
|
||||
AUTO,
|
||||
}
|
||||
}
|
||||
@@ -191,6 +191,7 @@ public final class Versions {
|
||||
chooser.getExtensionFilters().add(OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS
|
||||
? new FileChooser.ExtensionFilter(i18n("extension.bat"), "*.bat")
|
||||
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
|
||||
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
|
||||
File file = chooser.showSaveDialog(Controllers.getStage());
|
||||
if (file != null)
|
||||
new LauncherHelper(profile, account, id).makeLaunchScript(file);
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.upgrade;
|
||||
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.util.Pack200Utils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@@ -49,7 +50,7 @@ class HMCLDownloadTask extends FileDownloadTask {
|
||||
break;
|
||||
|
||||
case PACK_XZ:
|
||||
byte[] raw = Files.readAllBytes(target);
|
||||
byte[] raw = FileUtils.readAllBytes(target);
|
||||
try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw));
|
||||
JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) {
|
||||
Pack200Utils.unpack(in, out);
|
||||
|
||||
@@ -25,19 +25,20 @@ import org.jackhuang.hmcl.ui.CrashWindow;
|
||||
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.util.Collections.newSetFromMap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
/**
|
||||
@@ -45,28 +46,32 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
*/
|
||||
public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private static final Map<String, String> SOURCE = new HashMap<String, String>() {
|
||||
{
|
||||
put("javafx.fxml.LoadException", i18n("crash.NoClassDefFound"));
|
||||
put("Location is not set", i18n("crash.NoClassDefFound"));
|
||||
put("UnsatisfiedLinkError", i18n("crash.user_fault"));
|
||||
put("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound"));
|
||||
put("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.VerifyError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound"));
|
||||
put("javax.imageio.IIOException", i18n("crash.NoClassDefFound"));
|
||||
put("netscape.javascript.JSException", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound"));
|
||||
put("java.lang.ClassFormatError", i18n("crash.NoClassDefFound"));
|
||||
put("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java"));
|
||||
put("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?");
|
||||
}
|
||||
};
|
||||
// Lazy initialization resources
|
||||
private static final class Hole {
|
||||
@SuppressWarnings("unchecked")
|
||||
static final Pair<String, String>[] SOURCE = (Pair<String, String>[]) new Pair<?, ?>[]{
|
||||
pair("javafx.fxml.LoadException", i18n("crash.NoClassDefFound")),
|
||||
pair("Location is not set", i18n("crash.NoClassDefFound")),
|
||||
pair("UnsatisfiedLinkError", i18n("crash.user_fault")),
|
||||
pair("java.lang.NoClassDefFoundError", i18n("crash.NoClassDefFound")),
|
||||
pair("org.jackhuang.hmcl.util.ResourceNotFoundError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.VerifyError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.NoSuchMethodError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.NoSuchFieldError", i18n("crash.NoClassDefFound")),
|
||||
pair("javax.imageio.IIOException", i18n("crash.NoClassDefFound")),
|
||||
pair("netscape.javascript.JSException", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.IncompatibleClassChangeError", i18n("crash.NoClassDefFound")),
|
||||
pair("java.lang.ClassFormatError", i18n("crash.NoClassDefFound")),
|
||||
pair("com.sun.javafx.css.StyleManager.findMatchingStyles", i18n("launcher.update_java")),
|
||||
pair("NoSuchAlgorithmException", "Has your operating system been installed completely or is a ghost system?")
|
||||
};
|
||||
|
||||
static final Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
|
||||
}
|
||||
|
||||
private boolean checkThrowable(Throwable e) {
|
||||
String s = StringUtils.getStackTrace(e);
|
||||
for (HashMap.Entry<String, String> entry : SOURCE.entrySet())
|
||||
for (Pair<String, String> entry : Hole.SOURCE)
|
||||
if (s.contains(entry.getKey())) {
|
||||
if (StringUtils.isNotBlank(entry.getValue())) {
|
||||
String info = entry.getValue();
|
||||
@@ -85,8 +90,6 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static Set<String> CAUGHT_EXCEPTIONS = newSetFromMap(new ConcurrentHashMap<>());
|
||||
|
||||
private final boolean showCrashWindow;
|
||||
|
||||
public CrashReporter(boolean showCrashWindow) {
|
||||
@@ -102,18 +105,20 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler {
|
||||
if (!stackTrace.contains("org.jackhuang"))
|
||||
return;
|
||||
|
||||
if (CAUGHT_EXCEPTIONS.contains(stackTrace))
|
||||
if (Hole.CAUGHT_EXCEPTIONS.contains(stackTrace))
|
||||
return;
|
||||
CAUGHT_EXCEPTIONS.add(stackTrace);
|
||||
Hole.CAUGHT_EXCEPTIONS.add(stackTrace);
|
||||
|
||||
String text = "---- Hello Minecraft! Crash Report ----\n" +
|
||||
" Version: " + Metadata.VERSION + "\n" +
|
||||
" Time: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
|
||||
" Thread: " + t.toString() + "\n" +
|
||||
" Thread: " + t + "\n" +
|
||||
"\n Content: \n " +
|
||||
stackTrace + "\n\n" +
|
||||
"-- System Details --\n" +
|
||||
" Operating System: " + System.getProperty("os.name") + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" Operating System: " + OperatingSystem.SYSTEM_NAME + ' ' + OperatingSystem.SYSTEM_VERSION + "\n" +
|
||||
" System Architecture: " + Architecture.SYSTEM_ARCH_NAME + "\n" +
|
||||
" Java Architecture: " + Architecture.CURRENT_ARCH_NAME + "\n" +
|
||||
" Java Version: " + System.getProperty("java.version") + ", " + System.getProperty("java.vendor") + "\n" +
|
||||
" Java VM Version: " + System.getProperty("java.vm.name") + " (" + System.getProperty("java.vm.info") + "), " + System.getProperty("java.vm.vendor") + "\n" +
|
||||
" JVM Max Memory: " + Runtime.getRuntime().maxMemory() + "\n" +
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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;
|
||||
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public final class HMCLService {
|
||||
private HMCLService() {
|
||||
}
|
||||
|
||||
public static void openRedirectLink(String id) {
|
||||
FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/" + id);
|
||||
}
|
||||
}
|
||||
@@ -64,11 +64,11 @@ import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static java.lang.Class.forName;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static org.jackhuang.hmcl.Metadata.HMCL_DIRECTORY;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.SelfDependencyPatcher.DependencyDescriptor.JFX_DEPENDENCIES;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.platform.JavaVersion.CURRENT_JAVA;
|
||||
|
||||
@@ -81,26 +81,37 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies");
|
||||
public static final String CURRENT_ARCH_CLASSIFIER = currentArchClassifier();
|
||||
public static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
|
||||
|
||||
private static List<DependencyDescriptor> readDependencies() {
|
||||
String content;
|
||||
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
|
||||
content = IOUtils.readFullyAsString(in, UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
|
||||
}
|
||||
|
||||
private static String currentArchClassifier() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "linux";
|
||||
case ARM:
|
||||
case ARM32:
|
||||
return "linux-arm32-monocle";
|
||||
case ARM64:
|
||||
return "linux-aarch64";
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "mac";
|
||||
case ARM64:
|
||||
return "mac-aarch64";
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
switch (Architecture.JDK) {
|
||||
switch (Architecture.CURRENT_ARCH) {
|
||||
case X86_64:
|
||||
return "win";
|
||||
case X86:
|
||||
@@ -187,18 +198,6 @@ public final class SelfDependencyPatcher {
|
||||
|
||||
private static final String DEPENDENCIES_LIST_FILE = "/assets/openjfx-dependencies.json";
|
||||
|
||||
private static List<DependencyDescriptor> readDependencies() {
|
||||
String content;
|
||||
try (InputStream in = SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE)) {
|
||||
content = IOUtils.readFullyAsString(in, UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
return new Gson().fromJson(content, TypeToken.getParameterized(List.class, DependencyDescriptor.class).getType());
|
||||
}
|
||||
|
||||
private static final List<DependencyDescriptor> JFX_DEPENDENCIES = readDependencies();
|
||||
|
||||
/**
|
||||
* Patch in any missing dependencies, if any.
|
||||
*/
|
||||
@@ -206,7 +205,7 @@ public final class SelfDependencyPatcher {
|
||||
// Do nothing if JavaFX is detected
|
||||
try {
|
||||
try {
|
||||
forName("javafx.application.Application");
|
||||
Class.forName("javafx.application.Application");
|
||||
return;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
@@ -419,11 +418,7 @@ public final class SelfDependencyPatcher {
|
||||
}
|
||||
|
||||
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
String expectedHash = dependency.sha1();
|
||||
String actualHash = Hex.encodeHex(DigestUtils.digest("SHA-1", dependency.localPath()));
|
||||
if (!expectedHash.equalsIgnoreCase(actualHash)) {
|
||||
throw new ChecksumMismatchException("SHA-1", expectedHash, actualHash);
|
||||
}
|
||||
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
|
||||
@@ -65,7 +65,12 @@ public final class Locales {
|
||||
*/
|
||||
public static final SupportedLocale RU = new SupportedLocale(new Locale("ru"));
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU);
|
||||
/**
|
||||
* Japanese
|
||||
*/
|
||||
public static final SupportedLocale JA = new SupportedLocale(Locale.JAPANESE);
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = Lang.immutableListOf(DEFAULT, EN, ZH_CN, ZH, ES, RU, JA);
|
||||
|
||||
public static SupportedLocale getLocaleByName(String name) {
|
||||
if (name == null) return DEFAULT;
|
||||
@@ -80,6 +85,8 @@ public final class Locales {
|
||||
return ES;
|
||||
case "ru":
|
||||
return RU;
|
||||
case "ja":
|
||||
return JA;
|
||||
default:
|
||||
return DEFAULT;
|
||||
}
|
||||
@@ -91,6 +98,7 @@ public final class Locales {
|
||||
else if (locale == ZH_CN) return "zh_CN";
|
||||
else if (locale == ES) return "es";
|
||||
else if (locale == RU) return "ru";
|
||||
else if (locale == JA) return "ja";
|
||||
else if (locale == DEFAULT) return "def";
|
||||
else throw new IllegalArgumentException("Unknown locale: " + locale);
|
||||
}
|
||||
|
||||
@@ -95,6 +95,10 @@
|
||||
-fx-pref-width: 200;
|
||||
}
|
||||
|
||||
.bold {
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
|
||||
.memory-label {
|
||||
}
|
||||
|
||||
@@ -151,7 +155,7 @@
|
||||
-fx-padding: 0 0 0 10;
|
||||
}
|
||||
|
||||
.advanced-list-item > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
|
||||
.advanced-list-item > .rippler-container > .container > .two-line-list-item > .first-line > .title {
|
||||
-fx-font-size: 13;
|
||||
-fx-text-alignment: justify;
|
||||
}
|
||||
@@ -165,7 +169,7 @@
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
}
|
||||
|
||||
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
|
||||
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
@@ -178,7 +182,7 @@
|
||||
-fx-padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
|
||||
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
|
||||
-fx-font-size: 13;
|
||||
}
|
||||
|
||||
@@ -194,7 +198,7 @@
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
}
|
||||
|
||||
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
|
||||
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-font-weight: bold;
|
||||
}
|
||||
@@ -257,12 +261,7 @@
|
||||
-fx-padding: 4 0 4 0;
|
||||
}
|
||||
|
||||
.two-line-list-item > HBox {
|
||||
-fx-spacing: 8;
|
||||
-fx-alignment: center-left;
|
||||
}
|
||||
|
||||
.two-line-list-item > FlowPane > .title {
|
||||
.two-line-list-item > .first-line > .title {
|
||||
-fx-text-fill: #292929;
|
||||
-fx-font-size: 15px;
|
||||
-fx-padding: 0 8 0 0;
|
||||
@@ -274,7 +273,7 @@
|
||||
-fx-font-size: 12px;
|
||||
}
|
||||
|
||||
.two-line-list-item > FlowPane > .tag {
|
||||
.two-line-list-item > .first-line > .tag {
|
||||
-fx-text-fill: -fx-base-color;
|
||||
-fx-background-color: -fx-base-rippler-color;
|
||||
-fx-padding: 2;
|
||||
@@ -286,7 +285,7 @@
|
||||
|
||||
}
|
||||
|
||||
.two-line-item-second-large > FlowPane > .title {
|
||||
.two-line-item-second-large > .first-line > .title {
|
||||
-fx-text-fill: rgba(0, 0, 0, 0.5);
|
||||
-fx-font-weight: normal;
|
||||
-fx-font-size: 12px;
|
||||
@@ -307,7 +306,7 @@
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
|
||||
.bubble > HBox > .two-line-list-item > FlowPane > .title,
|
||||
.bubble > HBox > .two-line-list-item > .first-line > .title,
|
||||
.bubble > HBox > .two-line-list-item > HBox > .subtitle {
|
||||
-fx-text-fill: white;
|
||||
}
|
||||
@@ -748,7 +747,7 @@
|
||||
-fx-font-weight: BOLD;
|
||||
-fx-prompt-text-fill: #808080;
|
||||
-fx-alignment: top-left;
|
||||
-fx-pref-width: 300.0;
|
||||
-fx-max-width: 1000000000;
|
||||
-jfx-focus-color: -fx-base-check-color;
|
||||
-fx-padding: 8;
|
||||
-jfx-unfocus-color: transparent;
|
||||
@@ -1219,6 +1218,7 @@
|
||||
-jfx-unfocus-color: transparent;
|
||||
-fx-background-color: #f1f3f4;
|
||||
-fx-padding: 4;
|
||||
-fx-max-width: 1000000000;
|
||||
}
|
||||
|
||||
.jfx-combo-box-warning {
|
||||
|
||||
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=Your Microsoft account is n
|
||||
account.methods.microsoft.error.no_character=Account is missing a Minecraft Java profile. While the Microsoft account is valid, it does not own the game.
|
||||
account.methods.microsoft.error.unknown=Failed to log in. Microsoft respond with error code %d.
|
||||
account.methods.microsoft.logging_in=Logging in...
|
||||
account.methods.microsoft.hint=You should click "login" button and continue login process in newly opened browser window.
|
||||
account.methods.microsoft.manual=After clicking "login" button, you should finish authorization in the newly opened browser window. If the browser window failed to show, you can click here to copy the URL, and manually open it in your browser.
|
||||
account.methods.microsoft.hint=You should click "login" button, paste the device code shown here later and continue login process in newly opened browser window.
|
||||
account.methods.microsoft.manual=Your device code is <b>%1$s</b>, click here to copy. After clicking "login" button, you should finish authorization in the newly opened browser window. If the browser window failed to show, you can open %2$s manually.
|
||||
account.methods.microsoft.profile=Account Profile...
|
||||
account.methods.microsoft.snapshot=HMCL Snapshot version does not support Microsoft login.
|
||||
account.methods.microsoft.waiting_browser=Waiting for authorization in opened browser window...
|
||||
@@ -289,10 +289,16 @@ download.javafx.notes=Downloading necessary components for HMCL through network.
|
||||
download.javafx.component=Downloading the module %s
|
||||
download.javafx.prepare=Ready to download
|
||||
|
||||
exception.access_denied=It's denied by operating system to access file %s. Maybe we don't have permission to access this file, or this file has already been opened by other program.\n\
|
||||
Please check if current operating system user has permission to access that file.\n\
|
||||
For Windows users, you can also try the Resource Monitor, find if some programs is holding the file, try to close related program, or restart your computer.
|
||||
exception.artifact_malformed=The file cannot pass verification.
|
||||
|
||||
extension.bat=Windows Bat file
|
||||
extension.mod=Mod file
|
||||
extension.png=Image file
|
||||
extension.sh=Bash shell
|
||||
extension.ps1=PowerShell Script
|
||||
extension.sh=Bash Script
|
||||
|
||||
fatal.javafx.incompatible=Application cannot patch JavaFX on current Java environment below 11.\nPlease run this app using JDK 11 or higher or a JDK with JavaFX bundled.
|
||||
fatal.javafx.missing=JavaFX is missing.\nIf you are using Java 11 or later, please downgrade to Oracle JRE 8, or install BellSoft Liberica Full JRE.\nIf you are using other OpenJDK builds, please ensure OpenJFX is included.
|
||||
@@ -300,6 +306,7 @@ fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing on the cu
|
||||
fatal.config_loading_failure=The configuration is not accessible.\nPlease ensure Hello Minecraft! Launcher has read and write access to "%s" and the files in it.
|
||||
fatal.migration_requires_manual_reboot=The update is complete. Please reopen Hello Minecraft! Launcher.
|
||||
fatal.apply_update_failure=We're sorry, Hello Minecraft! Launcher couldn't finish the update because something went wrong.\nBut you can still manually finish the update by downloading Hello Minecraft! Launcher from %s.\nPlease consider reporting this issue to us.
|
||||
fatal.samba=If you are trying to run HMCL in a shared folder by Samba, HMCL may not working, please try updating your Java or running HMCL in a local folder.
|
||||
|
||||
feedback=Feedback
|
||||
feedback.add=Add Feedback
|
||||
@@ -353,7 +360,11 @@ game.crash.reason.fabric_warnings=Fabric gives some warnings:\n%1$s
|
||||
game.crash.reason.file_already_exists=Game cannot run because file %1$s already exists.\nIf it can be deleted, backup it, delete it and try again.
|
||||
game.crash.reason.file_changed=Game cannot run because file verification failed.\nIf you have modified the Minecraft primary jar, you need roll back the changes, or re-download the game.
|
||||
game.crash.reason.gl_operation_failure=Game crashed due to some mods, shader packs, texture packs.\nPlease disable the mods/shader packs/texture packs you are using and try again.
|
||||
game.crash.reason.graphics_driver=Game crashed due to a problem with your graphics driver.\nPlease update your graphics driver to the latest version.\nIf your computer has a discrete graphics card, you need to check whether the game uses itegrated/core graphics. If so, please run HMCL and game using discrete graphics card. If you still have these problems, you may consider getting a new graphics card or a new computer.
|
||||
game.crash.reason.graphics_driver=Game crashed due to a problem with your graphics driver.\n\
|
||||
Please update your graphics driver to the latest version.\n\
|
||||
If your computer has a discrete graphics card, you need to check whether the game uses integrated/core graphics. If so, please run HMCL and game using discrete graphics card. If you still have these problems, you may consider getting a new graphics card or a new computer.\n\
|
||||
If you do want to continue using the core graphics, please check if you are using Intel(R) Core(TM) 3000 series or older Processor. If so, for Minecraft 1.16.5 or older version, you need to downgrade Java to 1.8.0_51 or older version, or you must buy a dedicated graphics card or new computer.\n\
|
||||
Click <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">here (Chinese website)</a> for more information.
|
||||
game.crash.reason.illegal_access_error=Game crashed because of some mods.\nIf you recognize: %1$s, you may update or delete the mod and try again.
|
||||
game.crash.reason.jdk_9=Game cannot run because the Java version is too high.\nYou need download and install Java 8 and select the newly installed Java in game settings.
|
||||
game.crash.reason.jvm_32bit=Game crashed because memory allocation exceeds the limit of 32-bit Java VM.\nIf your OS is 64-bit, please install and use 64-bit Java. If your OS is 32-bit, you may reinstall a 64-bit OS or change a new computer.\nOr, you can disable automatic memory allocation and set maxmium memory size to 1024MB or below.
|
||||
@@ -396,7 +407,6 @@ install.change_version=Change version
|
||||
install.change_version.confirm=Are you sure you want to update %s from verison %s to %s?
|
||||
install.failed=Version failed to install
|
||||
install.failed.downloading=Failed to install due to some files not downloaded successfully
|
||||
install.failed.downloading.access_denied=Failed to download file: %1$s. Because it's denied by operating system to access file %2$s. Maybe we don't have permission to access this file, or this file has already been opened by other program. You can try to close related program, or restart your computer.
|
||||
install.failed.downloading.detail=Failed to download file: %s
|
||||
install.failed.downloading.timeout=Timed out while downloading the file: %s
|
||||
install.failed.install_online=Unable to recognize the provided installer file. If you are installing a mod, go to "Mods" page.
|
||||
@@ -449,12 +459,18 @@ launch.advice.not_enough_space=You have allocated too much memory, because the p
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s requires Java %2$s or later, are you willing to download one now?
|
||||
launch.advice.too_large_memory_for_32bit=You have allocated too much memory, because of your 32-Bit Java Runtime Environment, your game may crash. The maximum memory capacity for 32 bit systems is is 1024MB. Shall we continue launching?
|
||||
launch.advice.vanilla_linux_java_8=For Linux x86-64, Minecraft 1.12.2 and below can only run on Java 8.\nJava 9 and above versions cannot load 32-bit native libraries like liblwjgl.so.
|
||||
launch.advice.vanilla_x86=Minecraft currently does not provide official support for architectures other than x86 and x86-64.\nYou can try to download the corresponding native library of the platform and specify its placement path.
|
||||
launch.advice.vanilla_x86.translation=Minecraft currently does not provide official support for architectures other than x86 and x86-64.\nPlease use Java for x86-64 to run minecraft through translator, or download the corresponding native library of the platform and specify its placement path.
|
||||
launch.failed=Unable to launch
|
||||
launch.failed.cannot_create_jvm=Java virtual machine could not be created. Java arguments may cause issues. Please restart without JVM arguments.
|
||||
launch.failed.creating_process=Failed to create process. Check your Java path.
|
||||
launch.failed.command_too_long=The command length exceeds the limit so cannot create bat script. Please export it as PowerShell script.
|
||||
launch.failed.decompressing_natives=Unable to decompress native libraries.
|
||||
launch.failed.download_library=Unable to download library %s.
|
||||
launch.failed.executable_permission=Unable to add permission to the launch script
|
||||
launch.failed.executable_permission=Unable to add permission to the launch script.
|
||||
launch.failed.execution_policy=Set execution policy
|
||||
launch.failed.execution_policy.failed_to_set=Failed to set execution policy
|
||||
launch.failed.execution_policy.hint=The current execution policy prevents running of PowerShell scripts.\nClick 'Ok' to allow the current user to execute PowerShell scripts,or click 'Cancel' to keep the status quo.
|
||||
launch.failed.exited_abnormally=Game exited abnormally, please check the log, or ask someone for help.
|
||||
launch.failed.no_accepted_java=Cannot find the Java installation suitable for current game. If you think you have installed a suitable Java VM, you can manually select it in game settings.
|
||||
launch.state.dependencies=Dependencies
|
||||
@@ -475,6 +491,7 @@ launcher.background.classic=Classic
|
||||
launcher.background.choose=Choose a background image file
|
||||
launcher.background.default=Standard
|
||||
launcher.background.network=Network
|
||||
launcher.background.translucent=Translucent
|
||||
launcher.cache_directory=Directory for caching
|
||||
launcher.cache_directory.clean=Clear
|
||||
launcher.cache_directory.choose=Choose the directory for caching
|
||||
@@ -633,7 +650,8 @@ multiplayer.download.success=Dependencies initialization succeeded
|
||||
multiplayer.download.unsupported=Current operating system or architecure is unsupported.
|
||||
multiplayer.exit.after_ready=Multiplayer session broken. cato exitcode %d
|
||||
multiplayer.exit.before_ready=Multiplayer session failed to create. cato exitcode %d
|
||||
multiplayer.exit.timeout=Failed to connect to multiplayer server.
|
||||
multiplayer.exit.timeout.static_token=Failed to connect to multiplayer server. Please switch to dynamic token and try again.
|
||||
multiplayer.exit.timeout.dynamic_token=Failed to connect to multiplayer server.
|
||||
multiplayer.hint=Multiplayer functionality is experimental. Please give feedback.
|
||||
multiplayer.nat=Network Type Detection
|
||||
multiplayer.nat.hint=Network type detection will make it clear whether your network fulfills our requirement for multiplayer mode.
|
||||
@@ -686,13 +704,14 @@ multiplayer.session.join.error.connection=Failed to join multiplayer session. Ca
|
||||
multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer session.
|
||||
multiplayer.session.join.invitation_code=Invitation code
|
||||
multiplayer.session.join.invitation_code.error=Incorrect invitation code. Please obtain invitation code from the player who creates the multiplayer session.
|
||||
multiplayer.session.join.invitation_code.version=Versions of multiplayer functionalities are not the same among you.
|
||||
multiplayer.session.join.kicked=You have been kicked by the session holder. You will lost connection with the multiplayer session.
|
||||
multiplayer.session.join.kicked=Lost connection: %s.
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=The session holder does not accept your join request in time.
|
||||
multiplayer.session.join.kicked.kicked=You have been kicked by the session holder.
|
||||
multiplayer.session.join.kicked.version_not_matched=Versions of multiplayer functionalities are not the same among you.
|
||||
multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session.
|
||||
multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port.
|
||||
multiplayer.session.join.rejected=Your connection is rejected by the session holder.
|
||||
multiplayer.session.join.wait=Waiting for acceptation.
|
||||
multiplayer.session.join.wait_timeout=The session holder does not accept your join request in time.
|
||||
multiplayer.session.members=Room Members
|
||||
multiplayer.session.quit=Quit Room
|
||||
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?
|
||||
@@ -829,8 +848,11 @@ settings.game.exploration=Explore
|
||||
settings.game.fullscreen=Fullscreen
|
||||
settings.game.java_directory=Java Directory
|
||||
settings.game.java_directory.auto=Automatically selected
|
||||
settings.game.java_directory.bit=, %s-Bit
|
||||
settings.game.java_directory.auto.not_found=No suitable Java
|
||||
settings.game.java_directory.bit=%s-Bit
|
||||
settings.game.java_directory.choose=Choose Java Directory.
|
||||
settings.game.java_directory.invalid=Invalid Java Path
|
||||
settings.game.java_directory.template=%s (%s)
|
||||
settings.game.management=Manage
|
||||
settings.game.working_directory=Working Directory
|
||||
settings.game.working_directory.choose=Choose Working Directory
|
||||
|
||||
@@ -342,7 +342,7 @@ settings.advanced.launcher_visible=Visibilidad de launcher
|
||||
settings.advanced.minecraft_arguments=Minecraft Arguments
|
||||
settings.advanced.natives_directory=Loka biblioteko- vojo (LWJGL)
|
||||
settings.advanced.natives_directory.choose=Elektu lokan bibliotekvojn
|
||||
settings.advanced.natives_directory.default=Por defecto (.minecraft/versions/<version name>/natives/)
|
||||
settings.advanced.natives_directory.default=Por defecto (El lanzador proporciona la biblioteca local del juego)
|
||||
settings.advanced.no_jvm_args=No JVM Args por defecto
|
||||
settings.advanced.precall_command=Comando pre-inicio (será ejecutado antes de inicio del juego)
|
||||
settings.advanced.server_ip=Host de servidor
|
||||
@@ -355,7 +355,7 @@ settings.game.dimension=Dimensiones de ventana de juego
|
||||
settings.game.exploration=Explorar
|
||||
settings.game.fullscreen=Pantalla completa
|
||||
settings.game.java_directory=Directorio Java
|
||||
settings.game.java_directory.bit=, %s-Bit
|
||||
settings.game.java_directory.bit=%s-Bit
|
||||
settings.game.java_directory.choose=Escoja directorio Java.
|
||||
settings.game.management=Gestionar
|
||||
settings.game.working_directory=Directorio de función
|
||||
|
||||
@@ -0,0 +1,977 @@
|
||||
#
|
||||
# Hello Minecraft! Launcher
|
||||
# Copyright (C) 2021 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/>.
|
||||
#
|
||||
|
||||
# Contributors: zhixuan2333
|
||||
about=About
|
||||
about.copyright=著作権
|
||||
about.copyright.statement=Copyright(c)2021huangyuhui。
|
||||
about.author=Author
|
||||
about.author.statement=Bilibili: @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=テキスト全体のリンクをクリックします。
|
||||
about.community=コミュニティ
|
||||
about.community.discord=Discord
|
||||
about.community.discord.statement=チャットに参加しよう!
|
||||
about.community.kaiheila=KaiHeiLa
|
||||
about.community.kaiheila.statement=チャットに参加しよう!
|
||||
about.dependency=Dependencies
|
||||
about.legal=法的承認
|
||||
about.thanks_to=Thanks to
|
||||
about.thanks_to.bangbang93.statement=BMCLAPIダウンロードソースプロバイダー。
|
||||
about.thanks_to.contributors=問題、プルリクエストなどを介してこのプロジェクトに参加したすべての貢献者。
|
||||
about.thanks_to.contributors.statement=Hello Minecraftをサポートしてくれたオープンソースコミュニティに感謝します!ランチャー
|
||||
about.thanks_to.gamerteam.statement=デフォルトの背景画像プロバイダー。
|
||||
about.thanks_to.mcbbs=MCBBS
|
||||
about.thanks_to.mcbbs.statement=MCBBSダウンロードソースプロバイダー。
|
||||
about.thanks_to.mcmod=mcmod.cn
|
||||
about.thanks_to.mcmod.statement=Mod中国語の名前翻訳プロバイダー。
|
||||
about.thanks_to.noin=noin.cn
|
||||
about.thanks_to.noin.statement=マルチプレイヤーサービスプロバイダー。
|
||||
about.thanks_to.red_lnn.statement=デフォルトの背景画像プロバイダー。
|
||||
about.thanks_to.users=HMCLユーザーグループのメンバー
|
||||
about.thanks_to.users.statement=寄付、バグレポートなどに感謝します。
|
||||
about.thanks_to.yushijinhun.statement=authlib-インジェクターのサポート
|
||||
about.open_source=オープンソース
|
||||
about.open_source.statement=GPL v3(https://github.com/huanghongxun/HMCL/)
|
||||
|
||||
account=アカウント
|
||||
account.cape=マント
|
||||
account.character=キャラクター
|
||||
account.choose=キャラクターを選択する
|
||||
account.create=アカウントを追加
|
||||
account.create.microsoft=Microsoft Login
|
||||
account.create.yggdrasil=Mojang Login
|
||||
account.create.offline=Offline
|
||||
account.create.authlibInjector=サードパーティーアカウントを追加
|
||||
account.email=メールアドレス
|
||||
account.failed=エラーコード:%d
|
||||
account.failed.character_deleted=キャラクターが削除されました。
|
||||
account.failed.connect_authentication_server=認証サーバーに接続できません。インターネット接続を確認してください。
|
||||
account.failed.connect_injector_server=認証サーバーに接続できません。ネットワークをチェックして、URLが正しいことを確認してください。
|
||||
account.failed.injector_download_failure=authlib-injectorのダウンロードに失敗しました。ネットワークを確認して、別のダウンロードソースに切り替えてみてください。
|
||||
account.failed.invalid_credentials=パスワードが正しくないか、一時的にログインできません。
|
||||
account.failed.invalid_password=無効なパスワード
|
||||
account.failed.invalid_token=ログアウトし、パスワードを再入力してログインしてください。
|
||||
account.failed.migration=アカウントをMicrosoftアカウントに移行する必要があります。すでに移行されている場合は、代わりに移行されたMicrosoftアカウントにログインする必要があります。
|
||||
account.failed.no_character=このアカウントに文字がありません。
|
||||
account.failed.server_response_malformed=無効なサーバー応答。認証サーバーでエラーが発生している可能性があります。
|
||||
account.failed.wrong_account=不一致のアカウントでログインしました。
|
||||
account.injector.add=認証サーバーを追加します
|
||||
account.injector.empty=Empty(追加するには右側のプラスボタンをクリックしてください)
|
||||
account.injector.http=警告:このサーバーはHTTPを使用するため、パスワードはクリアテキストで送信されます。
|
||||
account.injector.link.register=登録
|
||||
account.injector.server=Authサーバー
|
||||
account.injector.server_url=サーバーのURL
|
||||
account.injector.server_name=サーバー名
|
||||
account.login=ログイン
|
||||
account.login.hint=パスワードは保存されません。
|
||||
account.login.refresh=再ログイン
|
||||
account.logout=ログアウト
|
||||
account.register=登録
|
||||
account.manage=アカウントリスト
|
||||
account.methods=ログインタイプ
|
||||
account.methods.authlib_injector=authlib-インジェクター
|
||||
account.methods.microsoft=Microsoft Account
|
||||
account.methods.microsoft.birth=誕生日の設定を編集する方法...
|
||||
account.methods.microsoft.close_page=Microsoftアカウントの認証が終了しました。後で終了するログイン手順がいくつか残っています。このページを今すぐ閉じることができます。
|
||||
account.methods.microsoft.error.add_family=まだ18歳ではないため、Minecraftをプレイするには、大人があなたを家族に追加する必要があります。
|
||||
account.methods.microsoft.error.add_family_probably=すでに18歳であるかどうかにかかわらず、アカウント設定を確認してください。そうでない場合は、アカウントを家族に追加する必要があります。
|
||||
account.methods.microsoft.error.country_unavailable=XBoxLiveはお住まいの国/地域ではご利用いただけません。
|
||||
account.methods.microsoft.error.missing_xbox_account=MicrosoftアカウントがXboxアカウントに接続されていません。続行する前に作成してください。
|
||||
account.methods.microsoft.error.no_character=アカウントにMinecraftJavaプロファイルがありません。 Microsoftアカウントは有効ですが、ゲームを所有していません。
|
||||
account.methods.microsoft.error.unknown=ログインに失敗しました。Microsoftはエラーコード%dで応答します。
|
||||
account.methods.microsoft.logging_in=ログイン...
|
||||
account.methods.microsoft.hint=「ログイン」ボタンをクリックして、新しく開いたブラウザウィンドウでログインプロセスを続行する必要があります。
|
||||
account.methods.microsoft.manual=「ログイン」ボタンをクリックした後、新しく開いたブラウザウィンドウで認証を完了する必要があります。ブラウザウィンドウが表示されない場合は、ここをクリックしてURLをコピーし、ブラウザで手動で開くことができます。
|
||||
account.methods.microsoft.profile=アカウントプロファイル..
|
||||
account.methods.microsoft.snapshot=HMCLスナップショットバージョンはMicrosoftログインをサポートしていません。
|
||||
account.methods.microsoft.waiting_browser=開いているブラウザウィンドウで認証を待機しています...
|
||||
account.methods.offline=オフライン
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUIDは、Minecraftのゲームキャラクターの一意の識別子です。 UUIDの生成方法は、ゲームランチャーによって異なります。 UUIDを他のランチャーによって生成されたものに変更すると、オフラインアカウントのバックパック内のゲームブロック/アイテムが残ることが約束されます。このオプションは専門家向けです。何をしているのかわからない限り、このオプションを変更することはお勧めしません。
|
||||
account.methods.offline.uuid.malformed=Malformed
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.methods.yggdrasil.migration=Microsoftアカウントに移行する
|
||||
account.methods.yggdrasil.migration.how=Microsoftアカウントに移行する方法は?
|
||||
account.methods.yggdrasil.purchase=Minecraftを購入する
|
||||
account.missing=アカウントなし
|
||||
account.missing.add=追加するにはここをクリック
|
||||
account.not_logged_in=ログインしていません
|
||||
account.password=パスワード
|
||||
account.skin=スキン
|
||||
account.skin.file=スキンイメージファイル
|
||||
account.skin.model=モデル
|
||||
account.skin.model.default=Classic (Steve)
|
||||
account.skin.model.slim=Slim (Alex)
|
||||
account.skin.type.csl_api=Blessing Skin
|
||||
account.skin.type.csl_api.location=アドレス
|
||||
account.skin.type.csl_api.location.hint=CustomSkinAPI
|
||||
account.skin.type.little_skin=LittleSkin
|
||||
account.skin.type.little_skin.hint=スキンのWebサイトでキャラクターを作成する必要があります。そして、このオフラインアカウントのスキンは、同じ名前のスキンWebサイトのキャラクターのスキンにバインドされます。
|
||||
account.skin.type.local_file=ローカルスキン画像ファイル
|
||||
account.skin.upload=スキンをアップロードする
|
||||
account.skin.upload.failed=スキンのアップロードに失敗しました
|
||||
account.skin.invalid_skin=認識されないスキンファイル
|
||||
account.username=ユーザー名
|
||||
|
||||
archive.author=作成者
|
||||
archive.date=公開日
|
||||
archive.game_version=ゲームバージョン
|
||||
archive.name=名前
|
||||
archive.version=バージョン
|
||||
|
||||
Assets.download=アセットのダウンロード
|
||||
Assets.download_all=アセットの整合性チェック
|
||||
Assets.index.malformed=アセットインデックスの形式が正しくありません。バージョン設定の[ゲームアセットファイルの更新]で再試行できます。
|
||||
|
||||
button.cancel=キャンセル
|
||||
button.change_source=ダウンロードソースの変更
|
||||
button.clear=クリア
|
||||
button.delete=削除
|
||||
button.edit=編集
|
||||
button.install=インストール
|
||||
button.export=エクスポート
|
||||
button.no=いいえ
|
||||
button.ok=OK
|
||||
button.refresh=リフレッシュ
|
||||
button.remove=削除
|
||||
button.remove.confirm=削除してもよろしいですか?この操作をロールバックすることはできません!
|
||||
button.save=保存
|
||||
button.save_as=名前を付けて保存
|
||||
button.select_all=すべて選択
|
||||
button.yes=はい
|
||||
|
||||
color.recent=推奨
|
||||
color.custom=カスタムカラー
|
||||
|
||||
crack.NoClassDefFound=「HelloMinecraft!Launcher」ソフトウェアが破損していないことを確認してください。
|
||||
crack.user_fault=OSまたはJava環境が正しくインストールされていない可能性があり、クラッシュする可能性があります。Javaランタイム環境またはコンピュータを確認してください。
|
||||
|
||||
curse.category.0=すべて
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/4471
|
||||
curse.category.4474=Sci-Fi
|
||||
curse.category.4481=小さい/軽い
|
||||
curse.category.4483=戦闘
|
||||
curse.category.4477=ミニゲーム
|
||||
curse.category.4478=クエスト
|
||||
curse.category.4484=マルチプレイヤー
|
||||
curse.category.4476=探索
|
||||
curse.category.4736=Skyblock
|
||||
curse.category.4475=アドベンチャーとRPG
|
||||
curse.category.4487=FTB
|
||||
curse.category.4480=マップベース
|
||||
curse.category.4479=ハードコア
|
||||
curse.category.4482=特大
|
||||
curse.category.4472=Tech
|
||||
curse.category.4473=魔法
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.423=地図と情報
|
||||
curse.category.426=Addons
|
||||
curse.category.434=武器、道具、武器
|
||||
curse.category.409=構造
|
||||
curse.category.4485=ブラッドマジック
|
||||
curse.category.420=ストレージ
|
||||
curse.category.429=インダストリアルクラフト
|
||||
curse.category.419=魔法
|
||||
curse.category.412=テクノロジー
|
||||
curse.category.4557=レッドストーン
|
||||
curse.category.428=Tinkerの構成
|
||||
curse.category.414=プレイヤートランスポート
|
||||
curse.category.4486=ラッキーブロック
|
||||
curse.category.432=Buildcraft
|
||||
curse.category.418=遺伝学
|
||||
curse.category.4671=Twitch統合
|
||||
curse.category.408=鉱石と資源
|
||||
curse.category.4773=CraftTweaker
|
||||
curse.category.430=Thaumcraft
|
||||
curse.category.422=アドベンチャーとRPG
|
||||
curse.category.413=処理中
|
||||
curse.category.417=エネルギー
|
||||
curse.category.415=エネルギー、流体、およびアイテムの輸送
|
||||
curse.category.433=林業
|
||||
curse.category.425=その他
|
||||
curse.category.4545=Applied Energistics 2
|
||||
curse.category.416=農業
|
||||
curse.category.421=APIとライブラリ
|
||||
curse.category.4780=ファブリック
|
||||
curse.category.424=Cosmetic
|
||||
curse.category.406=世界の世代
|
||||
curse.category.435=サーバーユーティリティ
|
||||
curse.category.411=モブ
|
||||
curse.category.407=バイオーム
|
||||
curse.category.427=熱膨張
|
||||
curse.category.410=ディメンション
|
||||
curse.category.436=食品
|
||||
curse.category.4558=レッドストーン
|
||||
curse.category.4843=自動化
|
||||
curse.category.4906=MCreator
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/6
|
||||
curse.category.399=スチームパンク
|
||||
curse.category.396=128x
|
||||
curse.category.398=512x以上
|
||||
curse.category.397=256x
|
||||
curse.category.405=その他
|
||||
curse.category.395=64x
|
||||
curse.category.400=フォトリアリスティック
|
||||
curse.category.393=16x
|
||||
curse.category.403=従来型
|
||||
curse.category.394=32x
|
||||
curse.category.404=アニメーション
|
||||
curse.category.4465=Modサポート
|
||||
curse.category.402=中世
|
||||
curse.category.401=モダン
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/17
|
||||
curse.category.4464=変更された世界
|
||||
curse.category.250=ゲームマップ
|
||||
curse.category.249=作成
|
||||
curse.category.251=パルクール
|
||||
curse.category.253=サバイバル
|
||||
curse.category.248=アドベンチャー
|
||||
curse.category.252=パズル
|
||||
|
||||
# https://addons-ecs.forgesvc.net/api/v2/category/section/4546
|
||||
curse.category.4551=ハードコアクエストモード
|
||||
curse.category.4548=ラッキーブロック
|
||||
curse.category.4556=進行
|
||||
curse.category.4752=ガジェットの構築
|
||||
curse.category.4553=CraftTweaker
|
||||
curse.category.4554=レシピ
|
||||
curse.category.4549=ガイドブック
|
||||
curse.category.4547=構成
|
||||
curse.category.4550=クエスト
|
||||
curse.category.4555=World Gen
|
||||
curse.category.4552=スクリプト
|
||||
|
||||
curse.sort.author=Author
|
||||
curse.sort.date_created=作成日
|
||||
curse.sort.last_updated=最終更新
|
||||
curse.sort.name=名前
|
||||
curse.sort.popularity=人気
|
||||
curse.sort.total_downloads=合計ダウンロード数
|
||||
|
||||
download=ダウンロード
|
||||
download.code.404=リモートサーバーにファイルが見つかりません:%s
|
||||
download.content=ゲームコンテンツ
|
||||
download.curseforge.customization=光と影、およびゲームのカスタマイズ
|
||||
download.existing=ファイルは既に存在するため、保存できません。 「名前を付けて保存」を選択して、ファイルを別の場所に保存できます。
|
||||
download.external_link=ダウンロードサイトを開く
|
||||
download.failed=%1$sのダウンロードに失敗しました、応答コード:%2 $ d
|
||||
download.failed.empty=候補者なし。戻るにはここをクリックしてください。
|
||||
download.failed.refresh=バージョンリストをダウンロードできません。ここをクリックして再試行してください。
|
||||
download.game=ゲームのダウンロード
|
||||
download.provider.mcbbs=MCBBS(https://www.mcbbs.net/)
|
||||
download.provider.bmclapi=BMCLAPI(ミラーソース)
|
||||
download.provider.mojang=Mojang(OptiFineはBMCLAPIにより提供されます) 推奨
|
||||
download.provider.official=公式ソースからロード
|
||||
download.provider.balanced=mcbbsのソースからロード
|
||||
download.provider.mirror=ミラーソースからロード
|
||||
download.javafx=必要なランタイムコンポーネントのダウンロード
|
||||
download.javafx.notes=ネットワークを介したHMCLに必要なコンポーネントのダウンロード。\n [ダウンロードソースの変更]ボタンをクリックして詳細を表示し、ダウンロードソースを選択するか、[キャンセル]ボタンをクリックして停止して終了します。\n注:ダウンロード速度が遅すぎます。ダウンロードソースを変更してみてください。
|
||||
download.javafx.component=モジュール%sをダウンロードしています
|
||||
download.javafx.prepare=ダウンロードする準備ができました
|
||||
|
||||
extension.bat=WindowsBatファイル
|
||||
extension.mod=Modファイル
|
||||
extension.png=画像ファイル
|
||||
extension.ps1=PowerShellスクリプト
|
||||
extension.sh=Bashスクリプト
|
||||
|
||||
fatal.javafx.incompatible=アプリケーションは11未満の現在のJava環境でJavaFXにパッチを適用できません。\nこのアプリは、JDK11以降またはJavaFXがバンドルされたJDKを使用して実行してください。
|
||||
fatal.javafx.missing=JavaFXがありません。\nJava11以降を使用している場合は、Oracle JRE 8にダウングレードするか、BellSoft Liberica FullJREをインストールしてください。\n他のOpenJDKビルドを使用している場合は、OpenJFXが含まれていることを確認してください。
|
||||
fatal.missing_dst_root_ca_x3=現在のJavaプラットフォームにDSTルートCAX3証明書がありません。\n引き続きHelloMinecraftを使用できます。ランチャーですが、一部のサイト(Let's Encryptによって発行された証明書を使用するサイトなど)に接続できず、ランチャーが正しく機能しない可能性があります。\n問題を解決するには、Javaランタイムを8u101以降に更新してください。
|
||||
fatal.config_loading_failure=構成にアクセスできません。\nHelloMinecraftを確認してください。 Launcherには、「%s」とその中のファイルへの読み取りおよび書き込みアクセス権があります。
|
||||
fatal.migration_requires_manual_reboot=更新が完了しました。 Hello Minecraftを再開してください!ランチャー。
|
||||
fatal.apply_update_failure=ごめんなさい、こんにちはMinecraft!何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。 %sからのランチャー。\nこの問題を報告することを検討してください。
|
||||
|
||||
feedback=フィードバック
|
||||
Feedback.add=フィードバックを追加
|
||||
Feedback.add.hint.search_before_add=新しいフィードバックを追加する前に、キーワードを検索して、報告する内容がすでに報告されているかどうかを確認する必要があります。持っている場合は、それを賛成して優先順位を上げることができます
|
||||
Feedback.add.hint.title=フィードバックのタイトルは、ニーズを簡潔に要約できる必要があります。 「問題がある」、「アイデアがある」、「ゲームが開かない」など、一般的な問題を一目で確認できないタイトルは使用できません。
|
||||
Feedback.add.hint.content=フィードバックコンテンツは、ニーズを完全かつ簡潔に表現する必要があります。問題が発生した場合は、ランチャーを開いた後にクリックされたボタンや、操作が実行された後にトリガーされる問題など、繰り返しパスを詳細に説明する必要があります。新しい機能を追加する場合は、プレーヤーがその機能を必要とする理由、その機能で解決できる問題、およびその機能の実装方法について詳しく説明する必要があります。
|
||||
Feedback.add.login=フィードバックの許可を得るには、HMCLアカウントにログイン/登録する必要があります。
|
||||
Feedback.add.permission=新しいフィードバックを追加するには、フィードバック権限を取得する必要があります。
|
||||
feedback.author=作成者
|
||||
feedback.content=コンテンツ
|
||||
Feedback.empty=条件を満たすアイテムはありません。
|
||||
Feedback.failed=ロードに失敗しました
|
||||
Feedback.failed.permission=フィードバックを送信する権限を持つアカウントにログインしている場合にのみ、キーワードで検索できます。
|
||||
Feedback.failed.too_frequently=頻繁すぎる。あとでもう一度試してみてください。
|
||||
Feedback.like=Like
|
||||
Feedback.login=HMCLアカウントにログインします
|
||||
feedback.response=応答
|
||||
Feedback.response.empty=応答なし
|
||||
feedback.state.accepted=承認済み
|
||||
Feedback.state.open=保留中
|
||||
Feedback.state.rejected=拒否されました
|
||||
feedback.title=タイトル
|
||||
feedback.type=タイプ
|
||||
Feedback.type.bug=バグレポート
|
||||
Feedback.type.feature=機能リクエスト
|
||||
Feedback.unlike=Unlike
|
||||
Feedback.version=ランチャーバージョン
|
||||
|
||||
file=ファイル
|
||||
|
||||
folder.config=Config
|
||||
folder.game=ゲームディレクトリ
|
||||
folder.mod=Mod
|
||||
folder.resourcepacks=リソースパック
|
||||
folder.saves=マップ
|
||||
folder.screenshots=スクリーンショット
|
||||
|
||||
game=ゲーム
|
||||
game.crash.feedback=問題は非常に一般的ですが、理由がわかりませんでしたか?フィードバックページでフィードバックをお寄せください。
|
||||
game.crash.info=ゲームステータス
|
||||
game.crash.reason=クラッシュアナライザー
|
||||
game.crash.reason.analyzing=分析中..
|
||||
game.crash.reason.block=ブロックが原因でゲームを実行できません。\nMCEditでこのブロックを削除するか、関連するModを直接削除できます。\nブロックタイプ:%1$s \nブロックの場所:%2 $ s
|
||||
game.crash.reason.bootstrap_failed=mod%1$sがクラッシュしたため、ゲームを実行できません。\n削除または更新を試みることができます。
|
||||
game.crash.reason.config=modが構成ファイルを解析できないため、ゲームを実行できません。\nMod%1$sは構成ファイル%2 $ sを解析できません。
|
||||
game.crash.reason.debug_crash=クラッシュが手動でトリガーされるため、ゲームを実行できません。\n実際、ゲームは無実であり、すべてあなたの責任です。
|
||||
game.crash.reason.duplicated_mod=modが重複しているためゲームを実行できません:%s。\n%s \n各Modは1つしかインストールできません。重複したmodを削除して、再試行してください。
|
||||
game.crash.reason.entity=エンティティが原因でゲームを実行できません。\nMCEditでこのエンティティを削除するか、関連するModを直接削除できます。\nエンティティタイプ:%1$s \nエンティティの場所:%2 $ s
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12以降は、現在インストールされているmodと互換性がありません。ファブリックを0.11.7にダウングレードする必要があります。
|
||||
game.crash.reason.fabric_warnings=Fabricはいくつかの警告を出します:\n%1$s
|
||||
game.crash.reason.file_already_exists=ファイル%1$sが既に存在するため、ゲームを実行できません。\n削除できる場合は、バックアップして削除し、再試行してください。
|
||||
game.crash.reason.file_changed=ファイルの検証に失敗したため、ゲームを実行できません。\nMinecraftプライマリjarを変更した場合は、変更をロールバックするか、ゲームを再ダウンロードする必要があります。
|
||||
game.crash.reason.gl_operation_failure=一部のモッド、シェーダーパック、テクスチャパックが原因でゲームがクラッシュしました。\n使用しているモッド/シェーダーパック/テクスチャパックを無効にして、再試行してください。
|
||||
game.crash.reason.graphics_driver=グラフィックスドライバーの問題によりゲームがクラッシュしました。\nグラフィックスドライバーを最新バージョンに更新してください。\nコンピューターにディスクリートグラフィックスカードがある場合は、ゲームでitegrated /を使用しているかどうかを確認する必要があります。コアグラフィックス。その場合は、ディスクリートグラフィックカードを使用してHMCLとゲームを実行してください。それでもこれらの問題が発生する場合は、新しいグラフィックカードまたは新しいコンピュータの入手を検討してください。
|
||||
game.crash.reason.illegal_access_error=一部のmodが原因でゲームがクラッシュしました。\n認識している場合:%1$s、modを更新または削除して、再試行してください。
|
||||
game.crash.reason.jdk_9=Javaのバージョンが高すぎるため、ゲームを実行できません。\nJava 8をダウンロードしてインストールし、ゲーム設定で新しくインストールしたJavaを選択する必要があります。
|
||||
game.crash.reason.jvm_32bit=メモリ割り当てが32ビットJavaVMの制限を超えたため、ゲームがクラッシュしました。\nOSが64ビットの場合は、64ビットJavaをインストールして使用してください。 OSが32ビットの場合は、64ビットOSを再インストールするか、新しいコンピュータを変更できます。\nまたは、自動メモリ割り当てを無効にして、最大メモリサイズを1024MB以下に設定できます。
|
||||
game.crash.reason.loading_crashed_forge=mod%1$s(%2 $ s)がクラッシュしたため、ゲームがクラッシュしました。\n削除または更新を試みることができます。
|
||||
game.crash.reason.loading_crashed_fabric=mod%1$sがクラッシュしたため、ゲームがクラッシュしました。\n削除または更新を試みることができます。
|
||||
game.crash.reason.memory_exceeded=JVMが割り当てるのに十分なメモリがないため、ゲームがクラッシュしました。\nこの問題は、ページサイズが小さすぎることが原因です。\nゲーム設定で自動メモリ割り当てをオフにし、メモリ割り当てを次のように調整する必要があります。システムが処理できる値。\nシステムのページサイズを十分な大きさに調整することもできます。
|
||||
game.crash.reason.mod=modが原因でゲームがクラッシュしました:%1$s。\nmodを更新または削除して、再試行できます。
|
||||
game.crash.reason.mod_resolution=modの解決に失敗したため、ゲームがクラッシュしました。\nFabricは次の情報を提供します:\n%s
|
||||
game.crash.reason.mod_resolution_collection=必要なmodバージョンに互換性がないため、ゲームがクラッシュしました。\n%1$sには%2 $ sが必要です。\nこれは、%2 $ sをアップグレードまたはダウングレードする必要があることを意味します。 %3 $ sをダウンロードする必要があります。
|
||||
game.crash.reason.mod_resolution_conflict=modが競合しているため、ゲームがクラッシュしました。\n%1$sが%2 $ sと競合しています。
|
||||
game.crash.reason.mod_resolution_missing=一部のmodが依存するmodがインストールされていないため、ゲームがクラッシュしました。\n%1$sではmodをインストールする必要があります:%2 $ s。\nこれは、%2 $をダウンロードする必要があることを意味します。 ■ダウンロードページまたはオンラインWebサイトから。
|
||||
game.crash.reason.mod_resolution_missing_minecraft=modが現在のMinecraftバージョンと互換性がないため、ゲームがクラッシュしました。\n%1$sにはMinecraftバージョン%2 $ sが必要です。\nインストールされているバージョンのmodを保持する場合は、変更する必要があります。ゲームバージョン。\n現在のゲームバージョンを引き続き使用する場合は、適切なmodを再インストールする必要があります。
|
||||
game.crash.reason.mod_resolution_mod_version=%1$s(バージョン:%2 $ s)
|
||||
game.crash.reason.mod_resolution_mod_version.any=%1$s(任意のバージョン)
|
||||
game.crash.reason.no_class_def_found_error=コードが不完全なためゲームを実行できません。\nゲームにmodがないか、一部のmodファイルが不完全であるか、一部のmodが現在のゲームと互換性がない可能性があります。\nゲームを再インストールする必要がある場合があります。およびmod、またはヘルプを要求します。\n%1$sがありません
|
||||
game.crash.reason.no_such_method_error=コードが不完全なためゲームを実行できません。\nゲームにmodがないか、一部のmodファイルが不完全であるか、一部のmodが現在のゲームと互換性がない可能性があります。\nゲームを再インストールする必要がある場合があります。と改造、または助けを求める。
|
||||
game.crash.reason.opengl_not_supported=グラフィックスドライバに問題があるため、ゲームを実行できません。\nOpenGLがサポートされていないため、リモートデスクトップモードですか、それともストリーミングモードですか。はいの場合は、元のコンピューターを使用してゲームを開始してください。\nまたは、グラフィックスドライバーを最新バージョンに更新して、ゲームを再起動してください。コンピューターに個別のグラフィックがある場合は、ゲームが統合/コアグラフィックを使用しているかどうかを確認する必要があります。その場合は、ディスクリートグラフィックカードを使用してHMCLとゲームを実行してみてください。それでもこれらの問題が発生する場合は、新しいグラフィックカードまたは新しいコンピュータの入手を検討してください。
|
||||
game.crash.reason.openj9=ゲームはOpenJ9仮想マシンでは実行できません。ゲーム設定でHotspotJava仮想マシンに切り替えて、ゲームを再起動してください。そうでない場合は、オンラインでダウンロードできます。
|
||||
game.crash.reason.out_of_memory=メモリ不足のためゲームを実行できません。\nこれは、メモリ割り当てが少なすぎるか、modが多すぎることが原因である可能性があります。\nゲーム設定でゲームメモリ割り当てを増やして、ゲームを実行できるようにすることができます。より大きなメモリ。\nそれでもこれらの問題が発生する場合は、より良いコンピュータを変更する必要があるかもしれません。
|
||||
game.crash.reason.resolution_too_high=解像度が高すぎるリソースパックを使用しているため、ゲームを実行できません\n解像度の低いリソースパックを使用するか、メモリの大きいグラフィックカードを購入する必要があります。
|
||||
game.crash.reason.stacktrace=不明。 [ログ]ボタンをクリックすると詳細を確認できます。\nModIDを表すキーワードがいくつかあります。オンラインで検索して理由を見つけることができます。\n%s
|
||||
game.crash.reason.too_old_java=Java仮想マシンのバージョンが低すぎるため、ゲームを実行できません。\nゲーム設定でJava仮想マシンの新しいバージョン(%1$s)に切り替えて、ゲームを再起動する必要があります。そうでない場合は、オンラインでダウンロードできます。
|
||||
game.crash.reason.unknown=不明。 「ログ」ボタンをクリックすると詳細を確認できます。
|
||||
game.crash.reason.unsatisfied_link_error=必要なネイティブライブラリがないため、ゲームを実行できません。\n不足しているネイティブライブラリ:%1$s。\nゲーム設定でネイティブライブラリパスオプションを変更した場合は、元に戻すことをお勧めします。標準モードに切り替えます。\nすでに標準モードになっている場合は、不足しているネイティブライブラリがmodまたはゲームに属しているかどうかを確認してください。 HMCLが原因であることが確実な場合は、フィードバックをお送りください。\nネイティブライブラリパスを本当にカスタマイズする必要がある場合は、ゲームに必要なすべてのネイティブライブラリをディレクトリに配置する必要があります。
|
||||
game.crash.title=ゲームがクラッシュしました
|
||||
game.directory=ゲームディレクトリ
|
||||
game.version=ゲームバージョン
|
||||
|
||||
help=ヘルプ
|
||||
help.doc=ドキュメント
|
||||
help.detail=データパック、modpackなどのメーカー向け。
|
||||
|
||||
input.email=ユーザー名はメールである必要があります。
|
||||
input.number=数値である必要があります。
|
||||
input.not_empty=必須フィールド
|
||||
input.url=有効なURLである必要があります。
|
||||
|
||||
install=新しい構成を作成
|
||||
install.change_version=バージョンの変更
|
||||
install.change_version.confirm=%sをバージョン%sから%sに更新してもよろしいですか?
|
||||
install.failed=バージョンのインストールに失敗しました
|
||||
install.failed.downloading=一部のファイルが正常にダウンロードされなかったため、インストールに失敗しました
|
||||
install.failed.downloading.access_denied=ファイルのダウンロードに失敗しました:%1$s。これは、オペレーティングシステムによってファイル%2 $ sへのアクセスが拒否されているためです。このファイルにアクセスする権限がないか、このファイルが他のプログラムによって既に開かれている可能性があります。関連するプログラムを閉じるか、コンピュータを再起動してみてください。
|
||||
install.failed.downloading.detail=ファイルのダウンロードに失敗しました:%s
|
||||
install.failed.downloading.timeout=ファイルのダウンロード中にタイムアウトしました:%s
|
||||
install.failed.install_online=提供されたインストーラーファイルを認識できません。 modをインストールする場合は、「Mods」ページに移動します。
|
||||
install.failed.malformed=少し前にダウンロードしたファイルの形式が正しくありません。この問題を解決するには、他のダウンロードプロバイダーに切り替えることができます。
|
||||
install.failed.optifine_conflict=Fabric、OptiFine、ForgeがMinecraft1.13に同時にインストールされます
|
||||
install.failed.optifine_forge_1.17=Minecraft 1.17.1の場合、OptiFine H1 Pre2以降のバージョンのみがForgeと互換性があり、スナップショットバージョンでインストールできます。
|
||||
install.failed.version_mismatch=ライブラリにはゲームバージョン%sが必要ですが、実際のバージョンは%sです。
|
||||
install.installer.change_version=%s、このバージョンは現在のゲームバージョンと互換性がありません。別のものを選択するには、ここをクリックしてください。
|
||||
install.installer.choose=%sバージョンを選択してください
|
||||
install.installer.depend=%sに依存
|
||||
install.installer.fabric=Fabric
|
||||
install.installer.fabric-api=Fabric API
|
||||
install.installer.fabric-api.warning=警告:Fabric APIはmodであり、新しいゲームのmodディレクトリにインストールされます。新しいゲームの実行ディレクトリを変更しないでください。変更すると、FabricAPIのインストールが失われます。その設定を変更したい場合は、FabricAPIを再インストールする必要があります。
|
||||
install.installer.forge=Forge
|
||||
install.installer.game=Minecraft
|
||||
install.installer.incompatible=%sと互換性がありません
|
||||
install.installer.install=インストール%s
|
||||
install.installer.install_offline=ファイルからのインストール/更新
|
||||
install.installer.install_offline.extension=Forge / OptiFineインストーラー
|
||||
install.installer.install_offline.tooltip=Forge / OptiFineインストールファイルのインポートをサポートします
|
||||
install.installer.install_online=オンラインでインストール
|
||||
install.installer.install_online.tooltip=Fabric、Forge、OptiFine、LiteLoaderのインストールをサポートします。
|
||||
install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=インストールされていません
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.version=%s
|
||||
install.modpack=modpackを導入
|
||||
install.new_game=新しいゲームをインストールする
|
||||
install.new_game.already_exists=このバージョンはすでに存在します。
|
||||
install.new_game.current_game_version=現在のゲームバージョン
|
||||
install.new_game.malformed=無効な名前
|
||||
install.select=操作を選択します
|
||||
install.success=正常にインストールされました
|
||||
|
||||
lang=日本語
|
||||
lang.default=システム言語を使用する
|
||||
|
||||
launch.advice.java.auto=現在選択されているJavaVMは、ゲームの要件を満たしていません。適切なJavaVMを選択することを許可しますか?または、ゲーム設定で適切なJavaVMを選択することもできます。
|
||||
launch.advice.java.modded_java_7=Minecraft 1.7.2以前では、Java7以前のバージョンが必要です。
|
||||
launch.advice.corrected=JVMの選択はすでに修正されています。選択したJavaバージョンを維持したい場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.uncorrected=ゲームを正常に開始できることが確実な場合は、ゲーム設定でJavaVMチェックを無効にすることができます。
|
||||
launch.advice.different_platform=OSは64ビットですが、Javaは32ビットです。 64ビットJavaをお勧めします。
|
||||
launch.advice.forge2760_liteloader=Forge 2760以降はLiteLoaderと互換性がないため、Forgeを2773以降にアップグレードすることを検討してください。打ち上げを続けましょうか。
|
||||
launch.advice.forge28_2_2_optifine=Forge28.2.2以降のバージョンはOptiFineと互換性がありません。 Forgeを28.2.1以前のバージョンにダウングレードすることを検討してください。打ち上げを続けましょうか。
|
||||
launch.advice.java8_1_13=Minecraft 1.13以降は、Java8以降でのみ実行できます。
|
||||
launch.advice.java8_51_1_13=Minecraft 1.13は、1.8.0_51より前のJava8でクラッシュする可能性があります。最新バージョンのJava8をインストールしてください。
|
||||
launch.advice.java9=Java9以降のバージョンのJavaでMinecraft1.12以前を起動することはできません。
|
||||
ゲームを高速化するには、launch.advice.newer_java=Java8をお勧めします。多くのMinecraft1.12以降、およびほとんどのModには、Java8が必要です。
|
||||
launch.advice.not_enough_space=割り当てたメモリが多すぎます。物理メモリサイズが%dMBであるため、ゲームがクラッシュする可能性があります。打ち上げを続けましょうか。
|
||||
launch.advice.require_newer_java_version=Minecraft%1$sにはJava%2 $ s以降が必要です。今すぐダウンロードしますか?
|
||||
launch.advice.too_large_memory_for_32bit=32ビットJavaランタイム環境が原因で、割り当てたメモリが多すぎるため、ゲームがクラッシュする可能性があります。 32ビットシステムの最大メモリ容量は1024MBです。打ち上げを続けましょうか。
|
||||
launch.advice.vanilla_linux_java_8=Linux x86-64の場合、Minecraft1.12.2以下はJava8でのみ実行できます。\nJava9以降のバージョンでは、liblwjgl.soなどの32ビットネイティブライブラリをロードできません。
|
||||
launch.advice.vanilla_x86=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nプラットフォームの対応するネイティブライブラリをダウンロードして、その配置パスを指定してみてください。
|
||||
launch.advice.vanilla_x86.translation=Minecraftは現在、x86およびx86-64以外のアーキテクチャの公式サポートを提供していません。\nJava for x86-64を使用して、トランスレータを介してminecraftを実行するか、プラットフォームの対応するネイティブライブラリをダウンロードして指定してくださいその配置パス。
|
||||
launch.failed=起動できません
|
||||
launch.failed.cannot_create_jvm=Java仮想マシンを作成できませんでした。 Java引数は問題を引き起こす可能性があります。 JVM引数なしで再起動してください。
|
||||
launch.failed.creating_process=プロセスの作成に失敗しました。 Javaパスを確認してください。
|
||||
launch.failed.command_too_long=コマンドの長さが制限を超えているため、batスクリプトを作成できません。 PowerShellスクリプトとしてエクスポートしてください。
|
||||
launch.failed.decompressing_natives=ネイティブライブラリを解凍できません。
|
||||
launch.failed.download_library=ライブラリ%sをダウンロードできません。
|
||||
launch.failed.executable_permission=起動スクリプトにアクセス許可を追加できません。
|
||||
launch.failed.execution_policy=実行ポリシーを設定します
|
||||
launch.failed.execution_policy.failed_to_set=実行ポリシーの設定に失敗しました
|
||||
launch.failed.execution_policy.hint=現在の実行ポリシーにより、PowerShellスクリプトの実行が禁止されています。\n現在のユーザーがPowerShellスクリプトを実行できるようにするには、[OK]をクリックするか、現状を維持するには[キャンセル]をクリックします。
|
||||
launch.failed.exited_abnormally=ゲームが異常終了しました。ログを確認するか、誰かに助けを求めてください。
|
||||
launch.failed.no_accepted_java=現在のゲームに適したJavaインストールが見つかりません。適切なJavaVMがインストールされていると思われる場合は、ゲーム設定で手動で選択できます。
|
||||
launch.state.dependencies=依存関係
|
||||
launch.state.done=完了
|
||||
launch.state.java=Javaバージョンの検出
|
||||
launch.state.logging_in=ログイン
|
||||
launch.state.modpack=modpackを読み込んでいます
|
||||
launch.state.waiting_launching=ゲームの起動
|
||||
launch.wrong_javadir=無効なJavaディレクトリ、デフォルトのJavaパスが適用されます。
|
||||
|
||||
launcher=ランチャー
|
||||
launcher.agreement=EULA
|
||||
launcher.agreement.accept=同意します
|
||||
launcher.agreement.decline=同意しません
|
||||
launcher.agreement.hint=このソフトウェアを使用するには、EULAに同意する必要があります。
|
||||
launcher.background=背景画像
|
||||
launcher.background.classic=クラシック
|
||||
launcher.background.choose=背景画像ファイルを選択してください
|
||||
launcher.background.default=標準
|
||||
launcher.background.network=ネットワーク
|
||||
launcher.cache_directory=キャッシュ用のディレクトリ
|
||||
launcher.cache_directory.clean=Clear
|
||||
launcher.cache_directory.choose=キャッシュするディレクトリを選択します
|
||||
launcher.cache_directory.default=標準
|
||||
launcher.cache_directory.disabled=無効
|
||||
launcher.cache_directory.invalid=無効なディレクトリ。デフォルト設定の復元。
|
||||
launcher.contact=お問い合わせ
|
||||
launcher.crash=Hello Minecraft!ランチャーがクラッシュしました!次のコンテンツをコピーして、MCBBS、Baidu Tieba、GitHub、またはMinecraftForumを介してフィードバックを送信してください。
|
||||
launcher.crash_out_dated=Hello Minecraft!ランチャーがクラッシュしました!ランチャーが古くなっています。ランチャーを更新してください!
|
||||
launcher.update_java=Javaを更新してください。
|
||||
|
||||
login.empty_username=ユーザー名を設定していません!
|
||||
login.enter_password=パスワードを入力してください。
|
||||
|
||||
logwindow.show_lines=行を表示
|
||||
logwindow.terminate_game=ゲームを終了する
|
||||
logwindow.title=ログ
|
||||
logwindow.autoscroll=Autoscroll
|
||||
logwindow.export_game_crash_logs=ゲームのクラッシュ情報をエクスポートする
|
||||
|
||||
main_page=ホーム
|
||||
|
||||
message.cancelled=操作がキャンセルされました
|
||||
message.confirm=確認
|
||||
message.copied=クリップボードにコピーされました
|
||||
message.default=Default
|
||||
message.doing=お待ちください
|
||||
message.downloading=ダウンロード中...
|
||||
message.error=エラー
|
||||
message.failed=操作に失敗しました
|
||||
message.info=Info
|
||||
message.success=ジョブは正常に完了しました
|
||||
message.unknown=不明
|
||||
message.warning=警告
|
||||
|
||||
modpack=Modpack
|
||||
modpack.choose=modpackを選択(zip)。
|
||||
modpack.choose.local=ローカルからmodpackファイルをインポート。
|
||||
modpack.choose.local.detail=modpackファイルをこのページにドラッグしてインストールできます
|
||||
modpack.choose.remote=インターネットからmodpackをインポート。
|
||||
modpack.choose.remote.detail=リモートmodpackファイルへの直接ダウンロードリンクが必要です
|
||||
modpack.choose.remote.tooltip=リモートmodpackファイルへの直接ダウンロードリンク
|
||||
modpack.desc=注意事項と変更ログを含めてmodpackを説明してください。マークダウンとオンライン写真がサポートされています。
|
||||
modpack.description=説明
|
||||
modpack.download=Modpackをダウンロード
|
||||
modpack.enter_name=modpackの名前を入力。
|
||||
modpack.export=Modpackをエクスポート
|
||||
modpack.export.as=Export Modpack As...
|
||||
modpack.file_api=Modpackダウンロードリンクプレフィックス
|
||||
modpack.files.blueprints=BuildCraftブループリント
|
||||
modpack.files.config=Modの構成
|
||||
modpack.files.dumps=NEIデバッグ出力
|
||||
modpack.files.hmclversion_cfg=ランチャー構成ファイル
|
||||
modpack.files.liteconfig=Mod構成ファイル
|
||||
modpack.files.mods=Mod
|
||||
modpack.files.mods.voxelmods=VoxelMods(VoxelMapを含む)オプション
|
||||
modpack.files.options_txt=ゲームオプション
|
||||
modpack.files.optionsshaders_txt=シェーダーオプション
|
||||
modpack.files.resourcepacks=リソース/テクスチャパック
|
||||
modpack.files.saves=保存されたマップ
|
||||
modpack.files.scripts=MineTweaker構成
|
||||
modpack.files.servers_dat=サーバーリスト
|
||||
modpack.install=%smodpackをインストールします
|
||||
modpack.installing=modpackのインストール
|
||||
modpack.introduction=Curse、MultiMC、HMCLmodpackがサポートされています。
|
||||
modpack.invalid=無効なmodpackファイル。
|
||||
modpack.mismatched_type=不適切なmodpackタイプ、現在のゲームは%s modpackですが、更新ファイルは%smodpackです。
|
||||
modpack.name=Modpack名
|
||||
modpack.not_a_valid_name=無効なmodpack名
|
||||
modpack.origin=Origin
|
||||
modpack.origin.url=公式ウェブサイト
|
||||
modpack.origin.mcbbs=MCBBS
|
||||
modpack.origin.mcbbs.prompt=スレッドID
|
||||
modpack.scan=このmodpackをスキャンしています
|
||||
modpack.task.install=Modpackのインポート
|
||||
modpack.task.install.error=このmodpackファイルは認識できません。 CurseおよびMultiMCmodpackのみがサポートされています。
|
||||
modpack.task.install.will=modpackをインストールします:
|
||||
modpack.type.curse=Curse
|
||||
modpack.type.curse.completion=Cursemodpackに関連するファイルをインストールします
|
||||
modpack.type.curse.tolerable_error=このCursemodpackのすべてのファイルのダウンロードを完了できません。対応するゲームバージョンを開始するときに、ダウンロードを再試行できます。ネットワークの問題により、数回再試行する場合があります。
|
||||
modpack.type.curse.error=このCursemodpackをインストールできません。再試行してください。
|
||||
modpack.type.curse.not_found=必要なリソースの一部が欠落しているため、ダウンロードできませんでした。最新バージョンまたは他のmodpackを検討してください。
|
||||
modpack.type.manual.warning=このmodpackをインポートする代わりに、おそらくこのmodpackファイルを直接解凍する必要があります。そして、バンドルされたランチャーを使用してゲームを起動します。このmodpackは、ランチャーによってエクスポートされるのではなく、.minecraftディレクトリを圧縮することによって手動で作成されます。 HMCLはこのmodpackのインポートを試みることができます、続行しますか?
|
||||
modpack.type.mcbbs=MCBBS標準
|
||||
modpack.type.mcbbs.export=Hello Minecraftでインポートできます!ランチャーとMultiMC
|
||||
modpack.type.multimc=MultiMC
|
||||
modpack.type.multimc.export=Hello Minecraftでインポートできます!ランチャーとMultiMC
|
||||
modpack.type.server=サーバー自動更新Modpack
|
||||
modpack.type.server.export=サーバーマネージャーがゲームクライアントをリモートで更新できるようにする
|
||||
modpack.type.server.malformed=不正なサーバーのmodpackマニフェスト
|
||||
modpack.unsupported=サポートされていないmodpack、HMCL、MultiMC、およびCursemodpackのみがサポートされています。
|
||||
modpack.update=ゲームアップデート
|
||||
modpack.wizard=modpackウィザードのエクスポート
|
||||
modpack.wizard.step.1=基本オプション
|
||||
modpack.wizard.step.1.title=modpackの基本オプションを設定します。
|
||||
modpack.wizard.step.2=ファイルを選択します
|
||||
modpack.wizard.step.2.title=modpackにファイルを追加します。
|
||||
modpack.wizard.step.3=Modpackタイプ
|
||||
modpack.wizard.step.3.title=modpackの形式を選択します。
|
||||
modpack.wizard.step.initialization.exported_version=エクスポートされたゲームバージョン
|
||||
modpack.wizard.step.initialization.force_update=可能であればmodpackを強制的に更新します
|
||||
modpack.wizard.step.initialization.include_launcher=ランチャーを含める
|
||||
modpack.wizard.step.initialization.save=エクスポート先...
|
||||
modpack.wizard.step.initialization.warning=modpackを作成する前に、ゲームが正常に起動できること、および\nMinecraftがリリースバージョンであることを確認する必要があります。\n再配布できないmodを追加しないでください。
|
||||
modpack.wizard.step.initialization.server=サーバーの自動更新modpackの詳細については、ここをクリックしてください
|
||||
|
||||
modrinth.category.adventure=Adventure
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=装飾
|
||||
modrinth.category.equipment=機器
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.food=Food
|
||||
modrinth.category.library=Library
|
||||
modrinth.category.magic=Magic
|
||||
modrinth.category.misc=Misc
|
||||
modrinth.category.storage=ストレージ
|
||||
modrinth.category.technology=テクノロジー
|
||||
modrinth.category.utility=ユーティリティ
|
||||
modrinth.category.worldgen=Worldgen
|
||||
|
||||
mods=Mods
|
||||
mods.add=modを追加
|
||||
mods.add.failed=mods%sの追加に失敗しました。
|
||||
mods.add.success=mods%sが正常に追加されました。
|
||||
mods.category=Category
|
||||
mods.check_updates=更新を確認
|
||||
mods.check_updates.current_version=Current
|
||||
mods.check_updates.failed=一部のファイルのダウンロードに失敗しました
|
||||
mods.check_updates.file=ファイル
|
||||
mods.check_updates.source=Source
|
||||
mods.check_updates.target_version=Target
|
||||
mods.check_updates.update=更新
|
||||
mods.choose_mod=modを選択してください
|
||||
mods.curseforge=CurseForge
|
||||
mods.dependencies=Dependencies
|
||||
mods.disable=無効にする
|
||||
mods.download=Modのダウンロード
|
||||
mods.download.title=Modダウンロード-%1s
|
||||
mods.enable=有効にする
|
||||
mods.manage=Mods
|
||||
mods.mcbbs=MCBBS
|
||||
mods.mcmod=MCMOD
|
||||
mods.mcmod.page=MCMOD
|
||||
mods.mcmod.search=MCMODで検索
|
||||
mods.modrinth=Modrinth
|
||||
mods.name=Name
|
||||
mods.not_modded=最初にmodloaderをインストールする必要があります(Fabric、Forge、またはLiteLoader)
|
||||
mods.restore=Restore
|
||||
mods.url=公式ページ
|
||||
|
||||
multiplayer=マルチプレイ
|
||||
multiplayer.agreement.prompt=マルチプレイヤーサービスの使用を開始する前に、まずEULAに同意する必要があります。
|
||||
multiplayer.download=マルチプレイヤーの依存関係をダウンロードする
|
||||
multiplayer.download.failed=マルチプレイヤーの初期化に失敗しました。一部のファイルはダウンロードできません
|
||||
multiplayer.download.success=依存関係の初期化に成功しました
|
||||
multiplayer.download.unsupported=現在のオペレーティングシステムまたはアーキテクチャはサポートされていません。
|
||||
multiplayer.exit.after_ready=マルチプレイヤーセッションが壊れています。 cato exitcode%d
|
||||
multiplayer.exit.before_ready=マルチプレイヤーセッションの作成に失敗しました。 cato exitcode%d
|
||||
multiplayer.exit.timeout=マルチプレイヤーサーバーへの接続に失敗しました。
|
||||
multiplayer.hint=マルチプレイヤー機能は実験的なものです。フィードバックをお願いします。
|
||||
multiplayer.nat=ネットワークタイプの検出
|
||||
multiplayer.nat.hint=ネットワークタイプの検出により、ネットワークがマルチプレイヤーモードの要件を満たしているかどうかが明確になります。
|
||||
multiplayer.nat.latency=レイテンシー
|
||||
multiplayer.nat.not_yet_tested=まだテストされていません
|
||||
multiplayer.nat.packet_loss_ratio=パケット損失率
|
||||
multiplayer.nat.testing=テスト
|
||||
multiplayer.nat.type=NATタイプ
|
||||
multiplayer.nat.type.blocked_udp=非常に悪い(ブロックされたUDP)
|
||||
multiplayer.nat.type.full_cone=Good(フルコーン)
|
||||
multiplayer.nat.type.open_access=Good(オープンアクセス)
|
||||
multiplayer.nat.type.port_restricted_cone=Medium(ポート制限付きコーン)
|
||||
multiplayer.nat.type.restricted_cone=Medium(制限付きコーン)
|
||||
multiplayer.nat.type.symmetric=Bad(対称)
|
||||
multiplayer.nat.type.symmetric_udp_firewall=Bad(UDPファイアウォールと対称)
|
||||
multiplayer.nat.type.unknown=不明`
|
||||
multiplayer.powered_by=マルチプレイヤーサービスは<a href="https://noin.cn">noin.cn</a>によって提供されます。 <a href="https://noin.cn/agreement">EULA</a>
|
||||
multiplayer.report=レポート
|
||||
multiplayer.relay=リレーモード
|
||||
multiplayer.relay.hint=静的トークンを持ち、テストされたNATタイプが不良対称であるユーザーの場合
|
||||
multiplayer.session=セッション
|
||||
multiplayer.session.name.format=%1$sの部屋
|
||||
multiplayer.session.name.motd=HMCLマルチプレイヤーセッション-%s
|
||||
multiplayer.session.close=部屋を閉じる
|
||||
multiplayer.session.close.warning=部屋を閉じた後、部屋に参加したすべてのプレイヤーは接続を失います。継続する?
|
||||
multiplayer.session.copy_room_code=招待コードをコピーする
|
||||
multiplayer.session.create=セッションの作成
|
||||
multiplayer.session.create.error.dynamic_token=マルチプレイヤーセッションの作成に失敗しました。後でもう一度やり直してください。
|
||||
multiplayer.session.create.error.static_token=マルチプレイヤーセッションの作成に失敗しました。動的トークンで再試行できます。
|
||||
multiplayer.session.create.hint=マルチプレイヤーセッションを作成する前に、実行中のゲームで[LANサーバーを開く]をクリックし、ゲームに表示されているポートを下の空白に入力する必要があります。
|
||||
multiplayer.session.create.join=接続リクエスト
|
||||
multiplayer.session.create.join.allow=すべての参加リクエストを受け入れます(無効にした場合、攻撃を防ぐために、参加リクエストを手動で受け入れる必要があります)。
|
||||
multiplayer.session.create.join.prompt=Player%sがマルチプレイヤーセッションに参加したいと考えています。承認?
|
||||
multiplayer.session.create.members=メンバー
|
||||
multiplayer.session.create.members.kick=キック
|
||||
multiplayer.session.create.members.kick.prompt=このプレーヤーをキックした後、このプレーヤーとの確立された接続は切断されます。
|
||||
multiplayer.session.create.name=セッション名
|
||||
multiplayer.session.create.port=ポート
|
||||
multiplayer.session.create.port.error=ゲームポートを検出できません。マルチプレイヤー機能を有効にするには、ゲームで[LANサーバーを開く]をクリックする必要があります。
|
||||
multiplayer.session.create.token=トークン
|
||||
multiplayer.session.create.token.apply=静的トークンを申請する
|
||||
multiplayer.session.create.token.prompt=デフォルトはランダム化されています。 noin.cn(中国のウェブサイト)で自分のトークンを申請することができます。
|
||||
multiplayer.session.error.already_started=Catoサービスはすでにローカルで開始されています。他のマルチプレイヤーサービスが提供されているかどうかを確認してください。または、タスクマネージャーでcatoプロセスを強制終了できます。
|
||||
multiplayer.session.error.file_not_found=cato実行可能ファイルが見つかりません。このページに入るときに、このファイルをダウンロードする必要があります。ウイルス対策ソフトウェアがファイルを分離したかどうかを確認してください。 trueの場合、ロールバックしてください。
|
||||
multiplayer.session.expired=マルチプレイヤーセッションの有効期限が切れています。続行するには、部屋を再作成または再参加する必要があります。
|
||||
multiplayer.session.hint=マルチプレイヤー機能を有効にするには、ゲームで[LANサーバーを開く]をクリックする必要があります。
|
||||
multiplayer.session.join=セッションに参加する
|
||||
multiplayer.session.join.error=マルチプレイヤーセッションへの参加に失敗しました
|
||||
multiplayer.session.join.error.connection=マルチプレイヤーセッションへの参加に失敗しました。接続を確立できません。
|
||||
multiplayer.session.join.hint=マルチプレイヤーセッションをすでに作成しているゲーマーから招待コードを取得する必要があります。
|
||||
multiplayer.session.join.invitation_code=招待コード
|
||||
multiplayer.session.join.invitation_code.error=不正な招待コード。マルチプレイヤーセッションを作成したプレイヤーから招待コードを入手してください。
|
||||
multiplayer.session.join.invitation_code.version=マルチプレイヤー機能のバージョンはあなたの間で同じではありません。
|
||||
multiplayer.session.join.kicked=セッションホルダーによってキックされました。マルチプレイヤーセッションとの接続が失われます。
|
||||
multiplayer.session.join.lost_connection=マルチプレイヤーセッションとの接続が失われました。セッションが作成者によって破棄されたか、このセッションとの接続を確立できない可能性があります。
|
||||
multiplayer.session.join.port.error=リスニングに使用できるローカルネットワークポートが見つかりません。 HMCLがポートでリッスンする権限を持っていることを確認してください。
|
||||
multiplayer.session.join.rejected=接続はセッション所有者によって拒否されます。
|
||||
multiplayer.session.join.wait=受け入れを待っています。
|
||||
multiplayer.session.join.wait_timeout=セッション所有者は時間内にあなたの参加リクエストを受け入れません。
|
||||
multiplayer.session.members=ルームメンバー
|
||||
multiplayer.session.quit=Quit Room
|
||||
multiplayer.session.quit.warning=部屋を終了した後、サーバーとの接続が失われます。継続する?
|
||||
multiplayer.session.username=ユーザー名
|
||||
multiplayer.state.connecting=接続中
|
||||
multiplayer.state.disconnected=マルチプレイヤーセッションが作成/開始されていません
|
||||
multiplayer.state.disconnected.hint=誰かがマルチプレイヤーセッションを作成し、他の人がセッションに参加して一緒にゲームをプレイする必要があります。
|
||||
multiplayer.state.master=作成された部屋:%1$s、ポート:%2 $ d
|
||||
multiplayer.state.master.hint=マルチプレイヤールームを作成したら、[招待コードをコピー]ボタンをクリックして、招待したいプレイヤーに招待コードを送信します。これらのプレーヤーは、会議室に参加するときに招待コードを貼り付けることができます。
|
||||
multiplayer.state.slave=参加ルーム:%1$s、アドレス:%2 $ s
|
||||
multiplayer.state.slave.copy=コピーアドレス
|
||||
multiplayer.state.slave.hint=マルチプレイヤールームに参加した後、Minecraftのマルチプレイヤーページにアクセスして「HMCLマルチプレイヤーセッション」サーバーに接続するか、以下に示すアドレスのサーバーを手動で追加する必要があります。
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=データパックをインストールします
|
||||
datapack.choose_datapack=インポートするデータパックのzipを選択してください
|
||||
datapack.extension=Datapack
|
||||
datapack.title=World%s-データパック
|
||||
|
||||
world=マップ
|
||||
world.add=マップを追加(.zip)
|
||||
world.datapack=データパックの管理
|
||||
world.datapack.1_13=Minecraft1.13以降のバージョンのみがデータパックをサポートします。
|
||||
world.description=%s。最終ゲーム時刻:%s。ゲームバージョン:%s。
|
||||
world.download=ダウンロード
|
||||
world.export=このマップをエクスポートする
|
||||
world.export.title=保存する場所を選択してください
|
||||
world.export.location=エクスポート先
|
||||
world.export.wizard=エクスポートワールド%s
|
||||
world.extension=World zip
|
||||
world.game_version=ゲームバージョン
|
||||
world.import.already_exists=このマップはすでに存在しています。
|
||||
world.import.choose=インポートするzipファイルを選択してください
|
||||
world.import.failed=このマップをインポートできません:%s
|
||||
world.import.invalid=無効なワールドzipファイル
|
||||
world.manage=マップ / データパック
|
||||
world.name=マップ名
|
||||
world.name.enter=マップ名を入力してください
|
||||
world.reveal=フォルダを表示
|
||||
world.show_all=すべて表示
|
||||
world.time=EEE、MMM d、yyyy HH:mm:ss
|
||||
|
||||
profile=ゲームディレクトリ
|
||||
profile.already_exists=この名前はすでに存在します。別の名前を使用してください。
|
||||
profile.default=現在のディレクトリ
|
||||
profile.home=標準ディレクトリ
|
||||
profile.instance_directory=ゲームディレクトリ
|
||||
profile.instance_directory.choose=ゲームディレクトリを選択してください
|
||||
profile.manage=ゲームディレクトリリスト
|
||||
profile.name=名前
|
||||
profile.new=新しい構成
|
||||
profile.title=ゲームディレクトリ
|
||||
profile.selected=選択済み
|
||||
profile.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
|
||||
|
||||
repositories.custom=カスタムMavenリポジトリ(%s)
|
||||
repositories.maven_central=Universal(Maven Central)
|
||||
repositories.aliyun_mirror=中国本土(Aliyun Mavenリポジトリ)
|
||||
repositories.chooser=JavaFXがないため、HMCLが機能するにはJavaFXが必要です。\n [OK]をクリックして指定したダウンロードソースからJavaFXランタイムコンポーネントをダウンロードしてHMCLを起動するか、[キャンセル]をクリックして終了します。\nダウンロードソースを選択:
|
||||
repositories.chooser.title=ダウンロードソースを選択してJavaFXをダウンロードします
|
||||
|
||||
resourcepack=リソースパック
|
||||
|
||||
search=検索
|
||||
search.hint.chinese=中国語と英語での検索をサポート
|
||||
search.hint.english=英語でのみ検索をサポート
|
||||
search.sort=Sort
|
||||
|
||||
selector.choose=選択
|
||||
selector.choose_file=ファイルを選択します
|
||||
selector.custom=カスタム
|
||||
|
||||
settings=設定
|
||||
|
||||
settings.advanced=詳細設定
|
||||
settings.advanced.custom_commands=カスタムコマンド
|
||||
settings.advanced.custom_commands.hint=カスタムコマンドは、次の環境変数を使用して実行されます。\n \
|
||||
\-$ INST_NAME:バージョンの名前\n \
|
||||
\-$ INST_ID:バージョンの名前\n \
|
||||
\-$ INST_DIR:バージョンの絶対パス\n \
|
||||
\-$ INST_MC_DIR:Minecraftの絶対パス\n \
|
||||
\-$ INST_JAVA:起動に使用されるJavaバイナリ\n \
|
||||
\-$ INST_FORGE:Forgeがインストールされている場合に設定\n \
|
||||
\-$ INST_LITELOADER:LiteLoaderがインストールされている場合に設定\n \
|
||||
\-$ INST_OPTIFINE:OptiFineがインストールされている場合に設定\n \
|
||||
\-$ INST_FABRIC:ファブリックがインストールされている場合に設定
|
||||
settings.advanced.dont_check_game_completeness=ゲームファイルをスキャンしない
|
||||
settings.advanced.dont_check_jvm_validity=JVMがゲームを起動できるかどうかを確認しないでください
|
||||
settings.advanced.game_dir.default=標準(.minecraft /)
|
||||
settings.advanced.game_dir.independent=バージョン独立(.minecraft / versions / <バージョン名> /、アセットとライブラリを除く)
|
||||
settings.advanced.java_permanent_generation_space=PermGen Space / MB
|
||||
settings.advanced.java_permanent_generation_space.prompt=Java 8以降のメタスペース、MB
|
||||
settings.advanced.jvm=Java仮想マシンの設定
|
||||
settings.advanced.jvm_args=JavaVM引数
|
||||
settings.advanced.jvm_args.prompt=ここに入力して、デフォルト設定を上書きします
|
||||
settings.advanced.launcher_visibility.close=ゲームの起動時にランチャーを閉じます。
|
||||
settings.advanced.launcher_visibility.hide=ゲームの起動時にランチャーを非表示にします。
|
||||
settings.advanced.launcher_visibility.hide_and_reopen=ランチャーを非表示にし、ゲームが終了したら再度開きます。
|
||||
settings.advanced.launcher_visibility.keep=ランチャーを表示したままにします。
|
||||
settings.advanced.launcher_visible=ランチャーの可視性
|
||||
settings.advanced.minecraft_arguments=Minecraftの引数
|
||||
settings.advanced.minecraft_arguments.prompt=デフォルト
|
||||
settings.advanced.natives_directory=ネイティブライブラリパス(例: LWJGL)
|
||||
settings.advanced.natives_directory.choose=ネイティブライブラリパスを選択してください
|
||||
settings.advanced.natives_directory.custom=Custom(ゲームが実行時に必要とするネイティブライブラリを提供する必要があります)
|
||||
settings.advanced.natives_directory.default=Standard(ネイティブライブラリは当社が提供します)
|
||||
settings.advanced.natives_directory.hint=このオプションは、AppleM1またはその他の公式にサポートされていないプラットフォーム/アーキテクチャでゲームをプレイする場合にのみ使用されます。\nこのオプションを変更する意味がわからない場合は、このオプションを変更しないことをお勧めします。 、またはゲームが起動しません。\nこのオプションを変更する必要がある場合は、ゲームを実行するために必要なすべてのネイティブライブラリをカスタムネイティブディレクトリの下に配置する必要があります。 lwjgl.dll(liblwjgl.so)、openal.dll(libopenal.so)。 HMCLは、不足しているネイティブライブラリを提供しません。
|
||||
settings.advanced.no_jvm_args=デフォルトのJVM引数なし
|
||||
settings.advanced.precall_command=起動前コマンド
|
||||
settings.advanced.precall_command.prompt=ゲーム開始前に実行されます
|
||||
settings.advanced.process_priority=プロセスの優先度
|
||||
settings.advanced.process_priority.low=低
|
||||
settings.advanced.process_priority.below_normal=通常以下
|
||||
settings.advanced.process_priority.normal=中
|
||||
settings.advanced.process_priority.above_normal=通常より上
|
||||
settings.advanced.process_priority.high=高
|
||||
settings.advanced.post_exit_command=終了後のコマンド
|
||||
settings.advanced.post_exit_command.prompt=ゲーム終了後に実行されます
|
||||
settings.advanced.server_ip=サーバーアドレス
|
||||
settings.advanced.server_ip.prompt=ゲームの起動時にサーバーに参加する
|
||||
settings.advanced.use_native_glfw=[Linux]システムGLFWを使用する
|
||||
settings.advanced.use_native_openal=[Linux]システムOpenALを使用する
|
||||
settings.advanced.workaround=Workarounds
|
||||
settings.advanced.workaround.warning=Workaround settings are only for professional users. Modifying workaround settings may result in game crash. Unless you know what you are doing, please do not modify these options.
|
||||
settings.advanced.wrapper_launcher=Wrapper command
|
||||
settings.advanced.wrapper_launcher.prompt=Allows launching using an extra wrapper program like 'optirun' on Linux.
|
||||
|
||||
settings.custom=カスタム
|
||||
|
||||
settings.game=ゲーム
|
||||
settings.game.current=ゲーム
|
||||
settings.game.dimension=ゲームウィンドウのサイズ
|
||||
settings.game.exploration=フォルダ
|
||||
settings.game.fullscreen=フルスクリーン
|
||||
settings.game.java_directory=Javaディレクトリ
|
||||
settings.game.java_directory.auto=自動的に選択されます
|
||||
settings.game.java_directory.bit=%s-ビット
|
||||
settings.game.java_directory.choose=Javaディレクトリを選択します。
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=作業ディレクトリ
|
||||
settings.game.working_directory.choose=作業ディレクトリを選択してください
|
||||
|
||||
settings.icon=ゲームアイコン
|
||||
|
||||
settings.launcher=設定
|
||||
settings.launcher.appearance=外観
|
||||
settings.launcher.common_path.tooltip=このアプリは、すべてのダウンロードをここにキャッシュします。
|
||||
settings.launcher.debug=デバッグ
|
||||
settings.launcher.download=ダウンロード
|
||||
settings.launcher.download.threads=スレッド
|
||||
settings.launcher.download.threads.auto=自動
|
||||
settings.launcher.download.threads.hint=同時実行性が大きすぎると、システムがフリーズする可能性があります。ダウンロード速度は、ICPと宛先サーバーの影響を受ける可能性があります。
|
||||
settings.launcher.download_source=ソースのダウンロード
|
||||
settings.launcher.download_source.auto=ダウンロードソースの自動選択
|
||||
settings.launcher.enable_game_list=メインページにバージョンリストを表示する
|
||||
settings.launcher.font=フォント
|
||||
settings.launcher.general=全般的
|
||||
settings.launcher.language=言語
|
||||
settings.launcher.launcher_log.export=ランチャーログのエクスポート
|
||||
settings.launcher.launcher_log.export.failed=ログのエクスポートに失敗しました
|
||||
settings.launcher.launcher_log.export.success=ログが%sにエクスポートされました
|
||||
settings.launcher.log=ログ
|
||||
settings.launcher.log.font=ログフォント
|
||||
settings.launcher.proxy=プロキシ
|
||||
settings.launcher.proxy.authentication=プロキシ認証
|
||||
settings.launcher.proxy.disable=システムプロキシを使用する
|
||||
settings.launcher.proxy.host=Host
|
||||
settings.launcher.proxy.http=HTTP
|
||||
settings.launcher.proxy.none=プロキシなし
|
||||
settings.launcher.proxy.password=パスワード
|
||||
settings.launcher.proxy.port=ポート
|
||||
settings.launcher.proxy.socks=Socks
|
||||
settings.launcher.proxy.username=アカウント
|
||||
settings.launcher.theme=テーマ
|
||||
settings.launcher.title_transparent=タイトルの透明性
|
||||
settings.launcher.version_list_source=バージョンリストソース
|
||||
|
||||
settings.memory=ゲームメモリ
|
||||
settings.memory.allocate.auto=最小%1 $ .1f GB /実際の%2 $ .1f GB
|
||||
settings.memory.allocate.auto.exceeded=最小%1 $ .1f GB /実際の%2 $ .1f GB(%3 $ .1f GB使用可能)
|
||||
settings.memory.allocate.manual=割り当て%1 $ .1f GB
|
||||
settings.memory.allocate.manual.exceeded=割り当て%1 $ .1f GB(%3 $ .1f GB使用可能)
|
||||
settings.memory.auto_allocate=メモリサイズは自動的に決定されます
|
||||
settings.memory.lower_bound=最小メモリ
|
||||
settings.memory.used_per_total=%1 $ .1f GB使用済み/%2 $ .1fGB合計
|
||||
settings.physical_memory=物理メモリサイズ
|
||||
settings.show_log=ログを表示
|
||||
settings.skin=オフラインアカウントのスキンの変更がサポートされるようになりました!アカウントページに移動して、今すぐ肌を変えてください!
|
||||
settings.tabs.installers=Installers
|
||||
settings.take_effect_after_restart=再起動後に有効になります
|
||||
settings.type=バージョン設定タイプ
|
||||
settings.type.global=グローバルゲーム設定(ゲーム間で共有されるすべての設定)
|
||||
settings.type.global.manage=グローバルゲーム設定
|
||||
settings.type.global.edit=グローバルゲーム設定を構成します
|
||||
settings.type.special.enable=このゲームの特殊な設定を有効にします
|
||||
settings.type.special.edit=現在のゲーム設定を構成します
|
||||
settings.type.special.edit.hint=現在のゲームバージョン%sで特殊な設定が有効になっているため、現在のページのオプションはそのゲームバージョンに適用されません。リンクをクリックして、現在のゲーム設定を構成します。
|
||||
|
||||
sponsor=寄付
|
||||
sponsor.bmclapi=ダウンロードサービスはBMCLAPIとMCBBSによって提供されます。詳細については、ここをクリックしてください。
|
||||
sponsor.hmcl=こんにちはマインクラフト! Launcherは、無料のオープンソースのMinecraftランチャーであり、ユーザーは複数の個別のMinecraftインストールを簡単に管理できます。私たちはAfdianを使用して、ホスティングとプロジェクト開発の費用を引き続き支払います。詳細については、ここをクリックしてください。
|
||||
|
||||
system.architecture=Arch
|
||||
system.operating_system=OS
|
||||
|
||||
update=更新
|
||||
update.accept=更新
|
||||
update.changelog=変更
|
||||
update.channel.dev=ベータ
|
||||
update.channel.dev.hint=ベータ版を使用しています。ベータ版には、リリースバージョンと比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。\n \
|
||||
ベータ版は不安定なはずです!\n \
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット<a href="https://discord.gg/jVvC7HfM6U">Discord</a>または<a href="https://kaihei.co/Kx7n3t">KaiHeiLa</a>で問題を報告します。\n \n \
|
||||
このヒントを非表示にするには、<a href="https://hmcl.huangyuhui.net/api/redirect/sponsor">寄付</a>するか、フィードバックに貢献をする必要があります。
|
||||
update.channel.dev.title=ベータ版のヒント
|
||||
update.channel.nightly=アルファ
|
||||
update.channel.nightly.hint=アルファ版を使用しています。これには、ベータ版およびリリース版と比較していくつかの追加機能が含まれている可能性があり、テストにのみ使用されます。\n \
|
||||
アルファ版は不安定です!\n \
|
||||
問題が発生した場合は、<a href="hmcl://settings/feedback">フィードバックページ</a>にアクセスして報告するか、チャット<a href="https://discord.gg/jVvC7HfM6U">Discord</a>または<a href="https://kaihei.co/Kx7n3t">KaiHeiLa</a>で問題を報告します。\n \n \
|
||||
このヒントを非表示にするには、<a href="https://hmcl.huangyuhui.net/api/redirect/sponsor">寄付</a>するか、フィードバックに印象的な貢献をする必要があります。
|
||||
update.channel.nightly.title=アルファ版のヒント
|
||||
update.channel.stable=リリース
|
||||
update.checking=更新の確認
|
||||
update.failed=更新の実行に失敗しました
|
||||
update.found=アップデートが利用可能です!
|
||||
update.newest_version=最新バージョン:%s
|
||||
update.bubble.title=利用可能な更新:%s
|
||||
update.bubble.subtitle=更新するにはここをクリックしてください
|
||||
update.note=警告:ベータ版には、より多くの機能とバグ修正が含まれている可能性がありますが、エラーが発生する可能性もあります。
|
||||
update.latest=これは最新バージョンです。
|
||||
update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。
|
||||
update.tooltip=更新
|
||||
|
||||
version=ゲーム
|
||||
version.cannot_read=ゲームのバージョンが見つかりません。自動インストールを続行できません。
|
||||
version.empty=ゲームなし
|
||||
version.empty.add=起動構成を作成
|
||||
version.empty.launch=起動するバージョンがありません。ダウンロードページからバージョンをインストールしてください。
|
||||
version.empty.hint=インストールされているゲームバージョンはありません。新しいゲームをダウンロードするには、ここをクリックしてください。
|
||||
version.game.old=Old
|
||||
version.game.release=リリース
|
||||
version.game.releases=リリース
|
||||
version.game.snapshot=スナップショット
|
||||
version.game.snapshots=Snapshots
|
||||
version.launch=プレイ
|
||||
version.launch.test=テストプレイ
|
||||
version.launch_script=起動スクリプトを作成する
|
||||
version.launch_script.failed=起動スクリプトを作成できません。
|
||||
version.launch_script.save=起動スクリプトを保存する
|
||||
version.launch_script.success=作成されたスクリプト: %s。
|
||||
version.manage=起動構成
|
||||
version.manage.clean=ゲームディレクトリをクリアする
|
||||
version.manage.clean.tooltip=ログのクリア、クラッシュレポート
|
||||
version.manage.duplicate=ゲームインスタンスをコピーする
|
||||
version.manage.duplicate.duplicate_save=コピー保存
|
||||
version.manage.duplicate.prompt=新しいバージョン名を入力してください
|
||||
version.manage.duplicate.confirm=
|
||||
version.manage.manage=バージョンの管理
|
||||
version.manage.manage.title=バージョンの管理-%1s
|
||||
version.manage.redownload_assets_index=ゲームアセットファイルの更新
|
||||
version.manage.remove=このバージョンを削除します
|
||||
version.manage.remove.confirm=このバージョン%sを削除してもよろしいですか?このバージョンを再度復元することはできません。
|
||||
version.manage.remove.confirm.trash=このバージョン%sを削除してもよろしいですか?このバージョンは、システムのゴミ箱に%sという名前で復元できます。
|
||||
version.manage.remove.confirm.independent=このバージョンは独立モードであるため、このバージョンを削除すると、このバージョンに属するすべての保存済みワールドも削除されます。このバージョン%sを削除しますか?
|
||||
version.manage.remove_libraries=ライブラリファイルを削除します
|
||||
version.manage.rename=このバージョンの名前を変更します
|
||||
version.manage.rename.message=このバージョンの新しい名前を入力してください
|
||||
version.manage.rename.fail=バージョンの名前を変更できませんでした。
|
||||
version.settings=設定
|
||||
version.update=modpackを更新します
|
||||
|
||||
wizard.prev=<前へ
|
||||
wizard.failed=失敗
|
||||
wizard.finish=終了
|
||||
wizard.next=次へ>
|
||||
@@ -359,7 +359,7 @@ settings.advanced.launcher_visible=Видимость лаунчера
|
||||
settings.advanced.minecraft_arguments=Параметры Minecraft
|
||||
settings.advanced.natives_directory=путь к локальной библиотеке (LWJGL)
|
||||
settings.advanced.natives_directory.choose=Выберите путь к локальной библиотеке
|
||||
settings.advanced.natives_directory.default=По умолчанию (.minecraft/versions/<версия>/natives/)
|
||||
settings.advanced.natives_directory.default=По умолчанию (Локальная библиотека игры предоставляется лаунчером)
|
||||
settings.advanced.no_jvm_args=По умолчанию параметры JVM отсутствуют
|
||||
settings.advanced.precall_command=Команда предзапуска (будет выполнено до запуска игры)
|
||||
settings.advanced.server_ip=Адрес сервера
|
||||
@@ -372,7 +372,7 @@ settings.game.dimension=Размер окна игры
|
||||
settings.game.exploration=Исследовать
|
||||
settings.game.fullscreen=На весь экран
|
||||
settings.game.java_directory=Каталог Java
|
||||
settings.game.java_directory.bit=, %s-бит
|
||||
settings.game.java_directory.bit=%s-бит
|
||||
settings.game.java_directory.choose=Выбор каталога Java.
|
||||
settings.game.management=Управление
|
||||
settings.game.working_directory=Рабочий каталог
|
||||
|
||||
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=你的微軟帳號尚未關
|
||||
account.methods.microsoft.error.no_character=該帳號沒有包含 Minecraft Java 版購買記錄
|
||||
account.methods.microsoft.error.unknown=登入失敗,錯誤碼:%d
|
||||
account.methods.microsoft.logging_in=登入中...
|
||||
account.methods.microsoft.hint=您需要點擊登入按鈕,並在新打開的瀏覽器窗口中完成登入。
|
||||
account.methods.microsoft.manual=若登入頁面未能打開,您可以點擊此處複製連結,並手動在瀏覽器中打開網頁。
|
||||
account.methods.microsoft.hint=您需要點擊登錄按鈕,並在新打開的瀏覽器窗口中完成登錄,並輸入待會在此處顯示的設備代碼。
|
||||
account.methods.microsoft.manual=設備代碼為:<b>%1$s</b>,已經幫你拷貝到剪切板中,如果未能黏貼,你可以點擊此處再複製一次。你需要在新打開的瀏覽器窗口中完成登錄,若登錄頁面未能打開,您手動在瀏覽器中打開網頁:%2$s。
|
||||
account.methods.microsoft.profile=帳戶設置頁
|
||||
account.methods.microsoft.snapshot=HMCL 快照版不支持微软登录
|
||||
account.methods.microsoft.waiting_browser=等待在新打開的瀏覽器窗口中完成登入...
|
||||
@@ -289,9 +289,15 @@ download.javafx.notes=正在通過網絡下載 HMCL 必要的運行時組件。\
|
||||
download.javafx.component=正在下載模塊 %s
|
||||
download.javafx.prepare=準備開始下載
|
||||
|
||||
exception.access_denied=因為無法訪問文件 %s,HMCL 沒有對該文件的訪問權限,或者該文件被其他程序打開。\n\
|
||||
請你檢查當前操作系統帳戶是否能訪問該文件,比如非管理員用戶可能不能訪問其他帳戶的個人文件夾內的文件。\n\
|
||||
對於 Windows 用戶,你還可以嘗試通過資源監視器查看是否有程序占用了該文件,如果是,你可以關閉占用此文件相關程序,或者重啟電腦再試。
|
||||
exception.artifact_malformed=下載的文件正確,無法通過校驗。
|
||||
|
||||
extension.bat=Windows 指令碼
|
||||
extension.mod=模組檔案
|
||||
extension.png=圖片檔案
|
||||
extension.ps1=PowerShell 指令碼
|
||||
extension.sh=Bash 指令碼
|
||||
|
||||
fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Java 11 且缺少 JavaFX 的 Java 環境上自行補全 JavaFX 運行環境。\n請更換如 Oracle Java 8 等包含 JavaFX 的 Java 運行環境,或者更新到 Java 11 或更高版本。
|
||||
@@ -300,6 +306,7 @@ fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 Hello Minecraft! Launcher 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 Hello Minecraft! Launcher。
|
||||
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 Hello Minecraft! Launcher 來完成升級。\n請考慮向我們回報該問題。
|
||||
fatal.samba=如果您正在通過 Samba 共亯的資料夾中運行 HMCL,HMCL 可能無法正常工作,請嘗試更新您的 Java 或在本地資料夾內運行 HMCL。
|
||||
|
||||
feedback=回饋
|
||||
feedback.add=新增回饋
|
||||
@@ -353,7 +360,11 @@ game.crash.reason.fabric_warnings=Fabric 提供了一些警告訊息:\n%1$s
|
||||
game.crash.reason.file_already_exists=當前遊戲因為文件 %1$s 已經存在,無法繼續運行。\n如果你認為這個文件可以刪除,你可以在備份這個文件後嘗試刪除它,並重新啟動遊戲。
|
||||
game.crash.reason.file_changed=當前遊戲因為檔案校驗失敗,無法繼續運行。\n如果你手動修改了 Minecraft.jar 檔案,你需要回退修改,或者重新下載遊戲。
|
||||
game.crash.reason.gl_operation_failure=當前遊戲因為你使用的某些 Mod、光影包、材質包,無法繼續運行。\n請先嘗試禁用你所使用的Mod/光影包/材質包再試。
|
||||
game.crash.reason.graphics_driver=當前遊戲因為你的顯示卡驅動存在問題崩潰了。\n請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n如果你的電腦存在獨立顯示卡,你需要檢查遊戲是否使用集成/核心顯示卡啟動,如果是,請嘗試使用獨立顯示卡啟動 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。
|
||||
game.crash.reason.graphics_driver=當前遊戲因為你的顯示卡驅動存在問題崩潰了。\n\
|
||||
請嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。\n\
|
||||
如果你的電腦存在獨立顯示卡,你需要檢查遊戲是否使用集成/核芯顯示卡啟動。如果是,請嘗試使用獨立顯示卡啟動 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。\n\
|
||||
如果你確實需要使用核芯顯示卡,請檢查你的電腦的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更舊的處理器,如果是,對於 Minecraft 1.16.5 及更舊版本,請你將遊戲所使用的 Java 版本降級至 1.8.0_51 及以下版本,否則你必須更換獨立顯示卡或新電腦。\n\
|
||||
詳情請點擊 <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">此處</a> 查看
|
||||
game.crash.reason.illegal_access_error=當前遊戲因為某些 Mod 的問題,無法繼續運行。\n如果你認識:%1$s,你可以更新或刪除對應 Mod 再試。
|
||||
game.crash.reason.jdk_9=當前遊戲因為 Java 版本過高,無法繼續運行。\n你需要下載安裝 Java 8,並在遊戲設置中將 Java 設置為 1.8 的版本。
|
||||
game.crash.reason.jvm_32bit=當前遊戲因為記憶體分配過大,超過了 32 位 Java 記憶體限制,無法繼續運行。\n如果你的電腦是 64 位系統,請下載安裝並更換 64 位 Java。如果你的電腦室 32 位系統,你或許可以重新安裝 64 位系統,或換一台新電腦。\n或者,你可以關閉遊戲記憶體的自動分配,並且把記憶體限制調節為 1024 MB 或以下。
|
||||
@@ -376,7 +387,7 @@ game.crash.reason.out_of_memory=當前遊戲因為記憶體不足,無法繼續
|
||||
game.crash.reason.resolution_too_high=當前遊戲因為材質包解析度過高,無法繼續運行\n你可以更換一個解析度更低的材質,或者更換一個視訊記憶體更大的顯示卡。
|
||||
game.crash.reason.stacktrace=原因未知,請點擊日誌按鈕查看詳細訊息。\n下面是一些關鍵字,其中可能包含 Mod 名稱,你可以透過搜索的方式查找有關訊息。\n%s
|
||||
game.crash.reason.too_old_java=當前遊戲因為 Java 虛擬機版本過低,無法繼續運行。\n你需要在遊戲設置中更換 %1$s 或更新版本的 Java 虛擬機,並重新啟動遊戲。如果沒有下載安裝,你可以在網路上自行下載。
|
||||
game.crash.reason.unknown=原因未知,請點擊日誌按鈕查看詳細訊息。
|
||||
game.crash.reason.unknown=原因未知,請點擊日誌按鈕查看詳細訊息。\n不要將本界面截圖給他人!如果你要求助他人,請你點擊左下角導出遊戲崩潰訊息後將導出的文件發送給他人以供分析!\n你可以向我們提供遊戲崩潰日誌以提升 HMCL 的日誌分析能力。
|
||||
game.crash.reason.unsatisfied_link_error=當前遊戲因為缺少本地庫,無法繼續運行。\n這些本地庫缺失:%1$s。\n如果你在遊戲設置中修改了本地庫路徑選項,請你修改回預設模式。\n如果已經在預設模式下,請檢查本地庫缺失是否是 Mod 引起的,或由 HMCL 引起的。如果你確定是 HMCL 引起的,建議你向我們回饋。\n如果你確實需要自訂本地庫路徑,你需要保證其中包含缺失的本地庫。
|
||||
game.crash.title=遊戲意外退出
|
||||
game.directory=遊戲路徑
|
||||
@@ -396,7 +407,6 @@ install.change_version=變更版本
|
||||
install.change_version.confirm=你確定要 %s 從 %s 更新到 %s 嗎?
|
||||
install.failed=安裝失敗
|
||||
install.failed.downloading=安裝失敗,部分檔案未能完成下載
|
||||
install.failed.downloading.access_denied=未能下載文件:%1$s。因為無法訪問文件 %2$s,HMCL 沒有對該文件的訪問權限,或者該文件被其他程序打開。你可以嘗試關閉相關程序,或者重啟電腦再試。
|
||||
install.failed.downloading.detail=未能下載檔案: %s
|
||||
install.failed.downloading.timeout=下載逾時: %s
|
||||
install.failed.install_online=無法識別要安裝的軟體。如果你要安裝 Mod,你需要在模組管理頁面安裝模組。
|
||||
@@ -449,12 +459,18 @@ launch.advice.not_enough_space=您設定的記憶體大小過大,由於超過
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s 僅能運行在 Java %2$s 或更高版本上,是否下載?
|
||||
launch.advice.too_large_memory_for_32bit=您設定的記憶體大小過大,由於可能超過了 32 位元 Java 的記憶體分配限制,所以可能無法啟動遊戲,請將記憶體調至低於 1024MB 的值。是否繼續啟動?
|
||||
launch.advice.vanilla_linux_java_8=對於 Linux x86-64 平台,Minecraft 1.12.2 及以下版本僅支持 Java 8。\nJava 9+ 版本會無法載入 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_x86=Minecraft 官方尚未提供對非 x86-64 平臺的支持。\n您可以嘗試下載您所用平臺對應的本機庫,在遊戲設定的調試選項中指定其路徑後再嘗試運行。
|
||||
launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供對非 x86-64 平臺的支持。\n請使用 x86-64 平臺的 Java 通過轉譯執行,或者嘗試下載您所用平臺對應的本機庫,在遊戲設定的調試選項中指定其路徑後再嘗試運行。
|
||||
launch.failed=啟動失敗
|
||||
launch.failed.cannot_create_jvm=偵測到無法建立 Java 虛擬機,可能是 Java 參數有問題。可以在設定中開啟無參數模式啟動。
|
||||
launch.failed.creating_process=啟動失敗,在建立新處理程式時發生錯誤。可能是 Java 路徑錯誤。
|
||||
launch.failed.command_too_long=命令長度超過限制,無法創建 bat 腳本,請匯出為 PowerShell 腳本。
|
||||
launch.failed.decompressing_natives=無法解壓縮遊戲資源庫。
|
||||
launch.failed.download_library=無法下載遊戲相依元件 %s。
|
||||
launch.failed.executable_permission=無法為啟動檔案新增執行權限。
|
||||
launch.failed.execution_policy=設定執行策略
|
||||
launch.failed.execution_policy.failed_to_set=設定執行策略失敗
|
||||
launch.failed.execution_policy.hint=當前執行策略封锁您執行 PowerShell 腳本。\n點擊“確定”允許當前用戶執行本地 PowerShell 腳本,或點擊“取消”保持現狀。
|
||||
launch.failed.exited_abnormally=遊戲非正常退出,請查看記錄檔案,或聯絡他人尋求幫助。
|
||||
launch.failed.no_accepted_java=找不到適合當前遊戲使用的 Java。如果您認為實際存在合適的 Java,您可以在遊戲設置中手動設置 Java。
|
||||
launch.state.dependencies=處理遊戲相依元件
|
||||
@@ -475,6 +491,7 @@ launcher.background.choose=選擇背景路徑
|
||||
launcher.background.classic=經典
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
|
||||
launcher.background.network=網路
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=檔案下載快取目錄
|
||||
launcher.cache_directory.clean=清理
|
||||
launcher.cache_directory.choose=選擇檔案下載快取目錄
|
||||
@@ -633,7 +650,8 @@ multiplayer.download.success=多人聯機初始化完成
|
||||
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台
|
||||
multiplayer.exit.after_ready=多人聯機會話意外退出,退出碼 %d
|
||||
multiplayer.exit.before_ready=多人聯機房間創建失敗,cato 退出碼 %d
|
||||
multiplayer.exit.timeout=無法連接多人聯機服務
|
||||
multiplayer.exit.timeout.static_token=無法連接多人聯機服務,請你切換動態 Token 重試。
|
||||
multiplayer.exit.timeout.dynamic_token=無法連接多人聯機服務,你可以在多人聯機頁面的回饋中回饋問題。
|
||||
multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請回饋。
|
||||
multiplayer.nat=網路檢測
|
||||
multiplayer.nat.hint=執行網路檢測可以讓你更清楚你的網路狀況是否符合聯機功能的需求。不符合聯機功能運行條件的網路狀況將可能導致聯機失敗。
|
||||
@@ -685,13 +703,14 @@ multiplayer.session.join.error.connection=加入房間失敗。無法與對方
|
||||
multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間
|
||||
multiplayer.session.join.invitation_code=邀請碼
|
||||
multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼
|
||||
multiplayer.session.join.invitation_code.version=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
|
||||
multiplayer.session.join.kicked=你已經被房主踢出房間,你將與房間失去連接。
|
||||
multiplayer.session.join.kicked=你與房間失去連接:%s。
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=對方未能即時同意你的加入申請
|
||||
multiplayer.session.join.kicked.kicked=你被房主踢出房間。
|
||||
multiplayer.session.join.kicked.version_not_matched=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
|
||||
multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
|
||||
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
|
||||
multiplayer.session.join.rejected=你被房主拒絕連接。
|
||||
multiplayer.session.join.wait=等待對方同意加入申請。
|
||||
multiplayer.session.join.wait_timeout=對方未能即時同意你的加入申請
|
||||
multiplayer.session.members=房間成員
|
||||
multiplayer.session.quit=退出房間
|
||||
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?
|
||||
@@ -828,8 +847,11 @@ settings.game.exploration=瀏覽
|
||||
settings.game.fullscreen=全螢幕
|
||||
settings.game.java_directory=Java 路徑
|
||||
settings.game.java_directory.auto=自動選擇合適的 Java
|
||||
settings.game.java_directory.bit=,%s 位
|
||||
settings.game.java_directory.auto.not_found=沒有合適的 Java
|
||||
settings.game.java_directory.bit=%s 位
|
||||
settings.game.java_directory.choose=選擇 Java 路徑
|
||||
settings.game.java_directory.invalid=Java 路徑不正確
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=執行路徑(版本隔離,修改後請自行移動相關遊戲檔案,如存檔模組設定等)
|
||||
settings.game.working_directory.choose=選擇執行路徑
|
||||
|
||||
@@ -96,8 +96,8 @@ account.methods.microsoft.error.missing_xbox_account=你的微软帐户尚未关
|
||||
account.methods.microsoft.error.no_character=该帐户没有包含 Minecraft Java 版购买记录
|
||||
account.methods.microsoft.error.unknown=登录失败,错误码:%d
|
||||
account.methods.microsoft.logging_in=登录中...
|
||||
account.methods.microsoft.hint=您需要点击登录按钮,并在新打开的浏览器窗口中完成登录。
|
||||
account.methods.microsoft.manual=若登录页面未能打开,您可以点击此处复制链接,并手动在浏览器中打开网页。
|
||||
account.methods.microsoft.hint=您需要点击登录按钮,并在新打开的浏览器窗口中完成登录,并输入待会在此处显示的设备代码。
|
||||
account.methods.microsoft.manual=设备代码为:<b>%1$s</b>,已经帮你拷贝到剪切板中,如果未能粘贴,你可以点击此处再复制一次。你需要在新打开的浏览器窗口中完成登录,若登录页面未能打开,您手动在浏览器中打开网页:%2$s。
|
||||
account.methods.microsoft.profile=帐户设置页
|
||||
account.methods.microsoft.snapshot=HMCL 快照版不支持微软登录
|
||||
account.methods.microsoft.waiting_browser=等待在新打开的浏览器窗口中完成登录...
|
||||
@@ -289,9 +289,15 @@ download.javafx.notes=正在通过网络下载 HMCL 必要的运行时组件。\
|
||||
download.javafx.component=正在下载模块 %s
|
||||
download.javafx.prepare=准备开始下载
|
||||
|
||||
exception.access_denied=因为无法访问文件 %s,HMCL 没有对该文件的访问权限,或者该文件被其他程序打开。\n\
|
||||
请你检查当前操作系统帐户是否能访问该文件,比如非管理员用户可能不能访问其他帐户的个人文件夹内的文件。\n\
|
||||
对于 Windows 用户,你还可以尝试通过资源监视器查看是否有程序占用了该文件,如果是,你可以关闭占用此文件相关程序,或者重启电脑再试。
|
||||
exception.artifact_malformed=下载的文件正确,无法通过校验。
|
||||
|
||||
extension.bat=Windows 脚本
|
||||
extension.mod=模组文件
|
||||
extension.png=图片文件
|
||||
extension.ps1=PowerShell 脚本
|
||||
extension.sh=Bash 脚本
|
||||
|
||||
fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHMCL 无法在低于 Java 11 且缺少 JavaFX 的 Java 环境上自行补全 JavaFX 运行环境。\n请更换如 Oracle Java 8 等包含 JavaFX 的 Java 运行环境,或者更新到 Java 11 或更高版本。
|
||||
@@ -300,6 +306,7 @@ fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件。\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限。
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 Hello Minecraft! Launcher。
|
||||
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题。\n但你依然可以从 %s 处手动下载 Hello Minecraft! Launcher 来完成升级。\n请考虑向我们反馈该问题。
|
||||
fatal.samba=如果您正在通过 Samba 共享的文件夹中运行 HMCL,HMCL 可能无法正常工作,请尝试更新您的 Java 或在本地文件夹内运行 HMCL。
|
||||
|
||||
feedback=反馈
|
||||
feedback.add=新增反馈
|
||||
@@ -353,7 +360,11 @@ game.crash.reason.fabric_warnings=Fabric 提供了一些警告信息:\n%1$s
|
||||
game.crash.reason.file_already_exists=当前游戏因为文件 %1$s 已经存在,无法继续运行。\n如果你认为这个文件可以删除,你可以在备份这个文件后尝试删除它,并重新启动游戏。
|
||||
game.crash.reason.file_changed=当前游戏因为文件校验失败,无法继续运行。\n如果你手动修改了 Minecraft.jar 文件,你需要回退修改,或者重新下载游戏。
|
||||
game.crash.reason.gl_operation_failure=当前游戏因为你使用的某些 Mod、光影包、材质包,无法继续运行。\n请先尝试禁用你所使用的Mod/光影包/材质包再试。
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了。\n请尝试升级你的显卡驱动到最新版本后再尝试启动游戏。\n如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核心显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了。\n\
|
||||
请尝试升级你的显卡驱动到最新版本后再尝试启动游戏。\n\
|
||||
如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核芯显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。\n\
|
||||
如果你确实需要使用核芯显卡,请检查你的电脑的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更旧的处理器,如果是,对于 Minecraft 1.16.5 及更旧版本,请你将游戏所使用的 Java 版本降级至 1.8.0_51 及以下版本,否则你必须更换独立显卡或新电脑。\n\
|
||||
详情请点击 <a href="https://hmcl.huangyuhui.net/help/game/graphics-card">此处</a> 查看
|
||||
game.crash.reason.illegal_access_error=当前游戏因为某些 Mod 的问题,无法继续运行。\n如果你认识:%1$s,你可以更新或删除对应 Mod 再试。
|
||||
game.crash.reason.jdk_9=当前游戏因为 Java 版本过高,无法继续运行。\n你需要下载安装 Java 8,并在游戏设置中将 Java 设置为 1.8 的版本。
|
||||
game.crash.reason.jvm_32bit=当前游戏因为内存分配过大,超过了 32 位 Java 内存限制,无法继续运行。\n如果你的电脑是 64 位系统,请下载安装并更换 64 位 Java。如果你的电脑室 32 位系统,你或许可以重新安装 64 位系统,或换一台新电脑。\n或者,你可以关闭游戏内存的自动分配,并且把内存限制调节为 1024 MB 或以下。
|
||||
@@ -376,7 +387,7 @@ game.crash.reason.out_of_memory=当前游戏因为内存不足,无法继续运
|
||||
game.crash.reason.resolution_too_high=当前游戏因为材质包分辨率过高,无法继续运行\n你可以更换一个分辨率更低的材质,或者更换一个显存更大的显卡。
|
||||
game.crash.reason.stacktrace=原因未知,请点击日志按钮查看详细信息。\n下面是一些关键词,其中可能包含 Mod 名称,你可以通过搜索的方式查找有关信息。\n%s
|
||||
game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 Java %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以在 https://adoptopenjdk.net 上自行下载。
|
||||
game.crash.reason.unknown=原因未知,请点击日志按钮查看详细信息。
|
||||
game.crash.reason.unknown=原因未知,请点击日志按钮查看详细信息。\n不要将本界面截图给他人!如果你要求助他人,请你点击左下角导出游戏崩溃信息后将导出的文件发送给他人以供分析!\n你可以向我们提供游戏崩溃日志以提升 HMCL 的日志分析能力。
|
||||
game.crash.reason.unsatisfied_link_error=当前游戏因为缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在游戏设置中修改了本地库路径选项,请你修改回预设模式。\n如果已经在预设模式下,请检查本地库缺失是否是 Mod 引起的,或由 HMCL 引起的。如果你确定是 HMCL 引起的,建议你向我们反馈。\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库。
|
||||
game.crash.title=游戏意外退出
|
||||
game.directory=游戏路径
|
||||
@@ -396,7 +407,6 @@ install.change_version=更换版本
|
||||
install.change_version.confirm=你确定要将 %s 从 %s 更新到 %s 吗?
|
||||
install.failed=安装失败
|
||||
install.failed.downloading=安装失败,部分文件未能完成下载
|
||||
install.failed.downloading.access_denied=未能下载文件:%1$s。因为无法访问文件 %2$s,HMCL 没有对该文件的访问权限,或者该文件被其他程序打开。你可以尝试关闭相关程序,或者重启电脑再试。
|
||||
install.failed.downloading.detail=未能下载文件:%s
|
||||
install.failed.downloading.timeout=下载超时:%s
|
||||
install.failed.install_online=无法识别要安装的软件。如果你要安装 Mod,你需要在模组管理页面安装模组。
|
||||
@@ -448,13 +458,19 @@ launch.advice.newer_java=检测到您未使用 Java 8 及更新版本,Java 8
|
||||
launch.advice.not_enough_space=您设置的内存大小过大,由于超过了系统内存大小 %dMB,所以可能影响游戏体验或无法启动游戏。是否继续启动?
|
||||
launch.advice.require_newer_java_version=Minecraft %1$s 仅能运行在 Java %2$s 或更高版本上,是否下载?
|
||||
launch.advice.too_large_memory_for_32bit=您设置的内存大小过大,由于可能超过了 32 位 Java 的内存分配限制,所以可能无法启动游戏,请将内存调至 1024MB 或更小。是否继续启动?
|
||||
launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及以下版本仅支持 Java 8。\nJava 9+ 版本会无法加载 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_linux_java_8=对于 Linux x86-64 平台,Minecraft 1.12.2 及以下版本仅支持 Java 8。\nJava 9+ 版本会无法加载 32 位的 liblwjgl.so。
|
||||
launch.advice.vanilla_x86=Minecraft 官方尚未提供对非 x86-64 平台的支持。\n您可以尝试下载您所用平台对应的本机库,在游戏设置的调试选项中指定其路径后再尝试运行。
|
||||
launch.advice.vanilla_x86.translation=Minecraft 官方尚未提供对非 x86-64 平台的支持。\n请使用 x86-64 平台的 Java 通过转译执行,或者尝试下载您所用平台对应的本机库,在游戏设置的调试选项中指定其路径后再尝试运行。
|
||||
launch.failed=启动失败
|
||||
launch.failed.cannot_create_jvm=截获到无法创建 Java 虚拟机,可能是 Java 参数有问题,可以在设置中开启无参数模式启动。
|
||||
launch.failed.creating_process=启动失败,在创建新进程时发生错误,可能是 Java 路径错误。
|
||||
launch.failed.command_too_long=命令长度超过限制,无法创建 bat 脚本,请导出为 PowerShell 脚本。
|
||||
launch.failed.decompressing_natives=未能解压游戏本地库。
|
||||
launch.failed.download_library=未能下载游戏依赖 %s.
|
||||
launch.failed.executable_permission=未能为启动文件添加执行权限。
|
||||
launch.failed.execution_policy=设置执行策略
|
||||
launch.failed.execution_policy.failed_to_set=设置执行策略失败
|
||||
launch.failed.execution_policy.hint=当前执行策略阻止您执行 PowerShell 脚本。\n点击“确定”允许当前用户执行本地 PowerShell 脚本,或点击“取消”保持现状。
|
||||
launch.failed.exited_abnormally=游戏非正常退出,请查看日志文件,或联系他人寻求帮助。
|
||||
launch.failed.no_accepted_java=找不到适合当前游戏使用的 Java。如果您认为实际存在合适的 Java,您可以在游戏设置中手动设置 Java。
|
||||
launch.state.dependencies=处理游戏依赖
|
||||
@@ -475,6 +491,7 @@ launcher.background.choose=选择背景路径
|
||||
launcher.background.classic=经典
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
|
||||
launcher.background.network=网络
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=文件下载缓存目录
|
||||
launcher.cache_directory.clean=清理
|
||||
launcher.cache_directory.choose=选择文件下载缓存目录
|
||||
@@ -633,7 +650,8 @@ multiplayer.download.success=多人联机初始化完成
|
||||
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台
|
||||
multiplayer.exit.after_ready=多人联机会话意外退出,退出码 %d
|
||||
multiplayer.exit.before_ready=多人联机房间创建失败,cato 退出码 %d
|
||||
multiplayer.exit.timeout=无法连接多人联机服务
|
||||
multiplayer.exit.timeout.static_token=无法连接多人联机服务,请你切换动态 Token 重试。
|
||||
multiplayer.exit.timeout.dynamic_token=无法连接多人联机服务,你可以在多人联机页面的反馈中反馈问题。
|
||||
multiplayer.hint=多人联机功能处于实验阶段,如果有问题请反馈。
|
||||
multiplayer.nat=网络检测
|
||||
multiplayer.nat.hint=执行网络检测可以让你更清楚你的网络状况是否符合联机功能的需求。检测结果为差的网络可能导致联机失败。
|
||||
@@ -685,13 +703,14 @@ multiplayer.session.join.error.connection=加入房间失败。无法与对方
|
||||
multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间
|
||||
multiplayer.session.join.invitation_code=邀请码
|
||||
multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码
|
||||
multiplayer.session.join.invitation_code.version=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
|
||||
multiplayer.session.join.kicked=你已经被房主踢出房间,你将与房间失去连接。
|
||||
multiplayer.session.join.kicked=你与房间失去连接:%s。
|
||||
multiplayer.session.join.kicked.join_acceptance_timeout=对方未能即时同意你的加入申请
|
||||
multiplayer.session.join.kicked.kicked=你被房主踢出房间。
|
||||
multiplayer.session.join.kicked.version_not_matched=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
|
||||
multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
|
||||
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
|
||||
multiplayer.session.join.rejected=你被房主拒绝连接。
|
||||
multiplayer.session.join.wait=等待对方同意加入申请。
|
||||
multiplayer.session.join.wait_timeout=对方未能即时同意你的加入申请
|
||||
multiplayer.session.members=房间成员
|
||||
multiplayer.session.quit=退出房间
|
||||
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?
|
||||
@@ -828,8 +847,11 @@ settings.game.exploration=浏览
|
||||
settings.game.fullscreen=全屏
|
||||
settings.game.java_directory=Java 路径
|
||||
settings.game.java_directory.auto=自动选择合适的 Java
|
||||
settings.game.java_directory.bit=,%s 位
|
||||
settings.game.java_directory.auto.not_found=没有合适的 Java
|
||||
settings.game.java_directory.bit=%s 位
|
||||
settings.game.java_directory.choose=选择 Java 路径
|
||||
settings.game.java_directory.invalid=Java 路径不正确
|
||||
settings.game.java_directory.template=%s(%s)
|
||||
settings.game.management=管理
|
||||
settings.game.working_directory=版本隔离(修改后请自行移动相关游戏文件,如存档模组配置等)
|
||||
settings.game.working_directory.choose=选择运行路径
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 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;
|
||||
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
|
||||
public final class JavaFXLauncher {
|
||||
|
||||
private JavaFXLauncher() {
|
||||
}
|
||||
|
||||
public static void start() {
|
||||
// init JavaFX Toolkit
|
||||
try {
|
||||
// Java 9 or Latter
|
||||
final MethodHandle startup =
|
||||
MethodHandles.publicLookup().findStatic(
|
||||
javafx.application.Platform.class, "startup", MethodType.methodType(void.class, Runnable.class));
|
||||
startup.invokeExact((Runnable) () -> {
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
// Java 8
|
||||
try {
|
||||
Class.forName("javafx.embed.swing.JFXPanel").getDeclaredConstructor().newInstance();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.ui;
|
||||
|
||||
import org.jackhuang.hmcl.JavaFXLauncher;
|
||||
import org.jackhuang.hmcl.game.ClassicVersion;
|
||||
import org.jackhuang.hmcl.game.LaunchOptions;
|
||||
import org.jackhuang.hmcl.launch.ProcessListener;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class GameCrashWindowTest {
|
||||
|
||||
@Test
|
||||
@Ignore
|
||||
public void test() throws Exception {
|
||||
JavaFXLauncher.start();
|
||||
|
||||
ManagedProcess process = new ManagedProcess(null, Arrays.asList("commands", "2"));
|
||||
|
||||
String logs = FileUtils.readText(new File("C:\\Users\\huang\\Downloads\\minecraft-exported-logs-2021-10-18T21-15-43.log"));
|
||||
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
FXUtils.runInFX(() -> {
|
||||
GameCrashWindow window = new GameCrashWindow(process, ProcessListener.ExitType.APPLICATION_ERROR, null,
|
||||
new ClassicVersion(),
|
||||
new LaunchOptions.Builder()
|
||||
.setJava(new JavaVersion(Paths.get("."), "16", Platform.SYSTEM_PLATFORM))
|
||||
.setGameDir(new File("."))
|
||||
.create(),
|
||||
Arrays.stream(logs.split("\\n"))
|
||||
.map(log -> pair(log, Log4jLevel.guessLevel(log)))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
window.showAndWait();
|
||||
|
||||
latch.countDown();
|
||||
});
|
||||
latch.await();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -31,7 +31,7 @@ public class MultiplayerClientServerTest {
|
||||
public void startServer() throws Exception {
|
||||
Logging.initForTest();
|
||||
int localPort = MultiplayerManager.findAvailablePort();
|
||||
MultiplayerServer server = new MultiplayerServer(1000, true);
|
||||
MultiplayerServer server = new MultiplayerServer("SessionName", 1000, true);
|
||||
server.startServer(localPort);
|
||||
|
||||
MultiplayerClient client = new MultiplayerClient("username", localPort);
|
||||
|
||||
@@ -13,5 +13,7 @@ dependencies {
|
||||
api group: 'com.github.steveice10', name: 'opennbt', version: '1.1'
|
||||
api group: 'com.nqzero', name: 'permit-reflect', version: '0.3'
|
||||
api group: 'org.nanohttpd', name: 'nanohttpd', version: '2.3.1'
|
||||
api group: 'org.apache.commons', name: 'commons-compress', version: '1.21'
|
||||
api group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
|
||||
compileOnlyApi group: 'org.jetbrains', name: 'annotations', version: '16.0.3'
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.auth;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.mapOf;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class OAuth {
|
||||
public static final OAuth MICROSOFT = new OAuth(
|
||||
"https://login.live.com/oauth20_authorize.srf",
|
||||
"https://login.live.com/oauth20_token.srf",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/devicecode",
|
||||
"https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
||||
|
||||
private final String authorizationURL;
|
||||
private final String accessTokenURL;
|
||||
private final String deviceCodeURL;
|
||||
private final String tokenURL;
|
||||
|
||||
public OAuth(String authorizationURL, String accessTokenURL, String deviceCodeURL, String tokenURL) {
|
||||
this.authorizationURL = authorizationURL;
|
||||
this.accessTokenURL = accessTokenURL;
|
||||
this.deviceCodeURL = deviceCodeURL;
|
||||
this.tokenURL = tokenURL;
|
||||
}
|
||||
|
||||
public Result authenticate(GrantFlow grantFlow, Options options) throws AuthenticationException {
|
||||
try {
|
||||
switch (grantFlow) {
|
||||
case AUTHORIZATION_CODE:
|
||||
return authenticateAuthorizationCode(options);
|
||||
case DEVICE:
|
||||
return authenticateDevice(options);
|
||||
default:
|
||||
throw new UnsupportedOperationException("grant flow " + grantFlow);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Result authenticateAuthorizationCode(Options options) throws IOException, InterruptedException, JsonParseException, ExecutionException, AuthenticationException {
|
||||
Session session = options.callback.startServer();
|
||||
options.callback.openBrowser(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"))));
|
||||
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("client_secret", options.callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(AuthorizationResponse.class);
|
||||
handleErrorResponse(response);
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
}
|
||||
|
||||
private Result authenticateDevice(Options options) throws IOException, InterruptedException, JsonParseException, AuthenticationException {
|
||||
DeviceTokenResponse deviceTokenResponse = HttpRequest.POST(deviceCodeURL)
|
||||
.form(pair("client_id", options.callback.getClientId()), pair("scope", options.scope))
|
||||
.ignoreHttpCode()
|
||||
.getJson(DeviceTokenResponse.class);
|
||||
handleErrorResponse(deviceTokenResponse);
|
||||
|
||||
options.callback.grantDeviceCode(deviceTokenResponse.userCode, deviceTokenResponse.verificationURI);
|
||||
|
||||
// Microsoft OAuth Flow
|
||||
options.callback.openBrowser(deviceTokenResponse.verificationURI);
|
||||
|
||||
long startTime = System.nanoTime();
|
||||
int interval = deviceTokenResponse.interval;
|
||||
|
||||
while (true) {
|
||||
Thread.sleep(Math.max(interval, 1));
|
||||
|
||||
// We stop waiting if user does not respond our authentication request in 15 minutes.
|
||||
long estimatedTime = System.nanoTime() - startTime;
|
||||
if (TimeUnit.SECONDS.convert(estimatedTime, TimeUnit.NANOSECONDS) >= Math.min(deviceTokenResponse.expiresIn, 900)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
TokenResponse tokenResponse = HttpRequest.POST(tokenURL)
|
||||
.form(
|
||||
pair("grant_type", "urn:ietf:params:oauth:grant-type:device_code"),
|
||||
pair("code", deviceTokenResponse.deviceCode),
|
||||
pair("client_id", options.callback.getClientId()))
|
||||
.ignoreHttpCode()
|
||||
.getJson(TokenResponse.class);
|
||||
|
||||
if ("authorization_pending".equals(tokenResponse.error)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("expired_token".equals(tokenResponse.error)) {
|
||||
throw new NoSelectedCharacterException();
|
||||
}
|
||||
|
||||
if ("slow_down".equals(tokenResponse.error)) {
|
||||
interval += 5;
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Result(tokenResponse.accessToken, tokenResponse.refreshToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Result refresh(String refreshToken, Options options) throws AuthenticationException {
|
||||
try {
|
||||
RefreshResponse response = HttpRequest.POST(accessTokenURL)
|
||||
.form(pair("client_id", options.callback.getClientId()),
|
||||
pair("client_secret", options.callback.getClientSecret()),
|
||||
pair("refresh_token", refreshToken),
|
||||
pair("grant_type", "refresh_token"))
|
||||
.accept("application/json")
|
||||
.ignoreHttpCode()
|
||||
.getJson(RefreshResponse.class);
|
||||
|
||||
handleErrorResponse(response);
|
||||
|
||||
return new Result(response.accessToken, response.refreshToken);
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleErrorResponse(ErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
public static class Options {
|
||||
private String userAgent;
|
||||
private final String scope;
|
||||
private final Callback callback;
|
||||
|
||||
public Options(String scope, Callback callback) {
|
||||
this.scope = scope;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public Options setUserAgent(String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Session {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
Session startServer() throws IOException, AuthenticationException;
|
||||
|
||||
void grantDeviceCode(String userCode, String verificationURI);
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
}
|
||||
|
||||
public enum GrantFlow {
|
||||
AUTHORIZATION_CODE,
|
||||
DEVICE,
|
||||
}
|
||||
|
||||
public class Result {
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
public Result(String accessToken, String refreshToken) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getRefreshToken() {
|
||||
return refreshToken;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DeviceTokenResponse extends ErrorResponse {
|
||||
@SerializedName("user_code")
|
||||
public String userCode;
|
||||
|
||||
@SerializedName("device_code")
|
||||
public String deviceCode;
|
||||
|
||||
// The URI to be visited for user.
|
||||
@SerializedName("verification_uri")
|
||||
public String verificationURI;
|
||||
|
||||
// Life time in seconds for device_code and user_code
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
// Polling interval
|
||||
@SerializedName("interval")
|
||||
public int interval;
|
||||
}
|
||||
|
||||
private static class TokenResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("ext_expires_in")
|
||||
public int extExpiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
}
|
||||
|
||||
private static class ErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response: {"error":"invalid_grant","error_description":"The provided
|
||||
* value for the 'redirect_uri' is not valid. The value must exactly match the
|
||||
* redirect URI used to obtain the authorization
|
||||
* code.","correlation_id":"??????"}
|
||||
*/
|
||||
public static class AuthorizationResponse extends ErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class RefreshResponse extends ErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.OAuth;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.RemoteAuthenticationException;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.Texture;
|
||||
@@ -36,11 +37,9 @@ import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
|
||||
import static java.util.Objects.requireNonNull;
|
||||
@@ -50,20 +49,15 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
public class MicrosoftService {
|
||||
private static final String AUTHORIZATION_URL = "https://login.live.com/oauth20_authorize.srf";
|
||||
private static final String ACCESS_TOKEN_URL = "https://login.live.com/oauth20_token.srf";
|
||||
private static final String SCOPE = "XboxLive.signin offline_access";
|
||||
private static final int[] PORTS = {29111, 29112, 29113, 29114, 29115};
|
||||
private static final ThreadPoolExecutor POOL = threadPool("MicrosoftProfileProperties", true, 2, 10,
|
||||
TimeUnit.SECONDS);
|
||||
private static final Pattern OAUTH_URL_PATTERN = Pattern
|
||||
.compile("^https://login\\.live\\.com/oauth20_desktop\\.srf\\?code=(.*?)&lc=(.*?)$");
|
||||
|
||||
private final OAuthCallback callback;
|
||||
private final OAuth.Callback callback;
|
||||
|
||||
private final ObservableOptionalCache<UUID, CompleteGameProfile, AuthenticationException> profileRepository;
|
||||
|
||||
public MicrosoftService(OAuthCallback callback) {
|
||||
public MicrosoftService(OAuth.Callback callback) {
|
||||
this.callback = requireNonNull(callback);
|
||||
this.profileRepository = new ObservableOptionalCache<>(uuid -> {
|
||||
LOG.info("Fetching properties of " + uuid);
|
||||
@@ -76,37 +70,11 @@ public class MicrosoftService {
|
||||
}
|
||||
|
||||
public MicrosoftSession authenticate() throws AuthenticationException {
|
||||
// Example URL:
|
||||
// https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=6a3728d6-27a3-4180-99bb-479895b8f88e&redirect_uri=http://localhost:29111/auth-response&scope=XboxLive.signin+offline_access&state=612fd24a2447427383e8b222b597db66&prompt=select_account
|
||||
try {
|
||||
// Microsoft OAuth Flow
|
||||
OAuthSession session = callback.startServer();
|
||||
callback.openBrowser(NetworkUtils.withQuery(AUTHORIZATION_URL,
|
||||
mapOf(pair("client_id", callback.getClientId()), pair("response_type", "code"),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE),
|
||||
pair("prompt", "select_account"))));
|
||||
String code = session.waitFor();
|
||||
|
||||
// Authorization Code -> Token
|
||||
String responseText = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||
.form(mapOf(pair("client_id", callback.getClientId()), pair("code", code),
|
||||
pair("grant_type", "authorization_code"), pair("client_secret", callback.getClientSecret()),
|
||||
pair("redirect_uri", session.getRedirectURI()), pair("scope", SCOPE)))
|
||||
.getString();
|
||||
LiveAuthorizationResponse response = JsonUtils.fromNonNullJson(responseText,
|
||||
LiveAuthorizationResponse.class);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.authenticate(OAuth.GrantFlow.DEVICE, new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (InterruptedException e) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof InterruptedException) {
|
||||
throw new NoSelectedCharacterException();
|
||||
} else {
|
||||
throw new ServerDisconnectException(e);
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
throw new ServerResponseMalformedException(e);
|
||||
}
|
||||
@@ -114,19 +82,8 @@ public class MicrosoftService {
|
||||
|
||||
public MicrosoftSession refresh(MicrosoftSession oldSession) throws AuthenticationException {
|
||||
try {
|
||||
LiveRefreshResponse response = HttpRequest.POST(ACCESS_TOKEN_URL)
|
||||
.form(pair("client_id", callback.getClientId()),
|
||||
pair("client_secret", callback.getClientSecret()),
|
||||
pair("refresh_token", oldSession.getRefreshToken()),
|
||||
pair("grant_type", "refresh_token"))
|
||||
.accept("application/json")
|
||||
.ignoreHttpErrorCode(400)
|
||||
.ignoreHttpErrorCode(401)
|
||||
.getJson(LiveRefreshResponse.class);
|
||||
|
||||
handleLiveErrorMessage(response);
|
||||
|
||||
return authenticateViaLiveAccessToken(response.accessToken, response.refreshToken);
|
||||
OAuth.Result result = OAuth.MICROSOFT.refresh(oldSession.getRefreshToken(), new OAuth.Options(SCOPE, callback));
|
||||
return authenticateViaLiveAccessToken(result.getAccessToken(), result.getRefreshToken());
|
||||
} catch (IOException e) {
|
||||
throw new ServerDisconnectException(e);
|
||||
} catch (JsonParseException e) {
|
||||
@@ -134,22 +91,6 @@ public class MicrosoftService {
|
||||
}
|
||||
}
|
||||
|
||||
private void handleLiveErrorMessage(LiveErrorResponse response) throws AuthenticationException {
|
||||
if (response.error == null || response.errorDescription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (response.error) {
|
||||
case "invalid_grant":
|
||||
if (response.errorDescription.contains("The user must sign in again and if needed grant the client application access to the requested scope")) {
|
||||
throw new CredentialExpiredException();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
throw new RemoteAuthenticationException(response.error, response.errorDescription, "");
|
||||
}
|
||||
|
||||
private String getUhs(XBoxLiveAuthenticationResponse response, String existingUhs) throws AuthenticationException {
|
||||
if (response.errorCode != 0) {
|
||||
throw new XboxAuthorizationException(response.errorCode, response.redirectUrl);
|
||||
@@ -357,57 +298,6 @@ public class MicrosoftService {
|
||||
public static class NoXuiException extends AuthenticationException {
|
||||
}
|
||||
|
||||
public static class LiveErrorResponse {
|
||||
@SerializedName("error")
|
||||
public String error;
|
||||
|
||||
@SerializedName("error_description")
|
||||
public String errorDescription;
|
||||
|
||||
@SerializedName("correlation_id")
|
||||
public String correlationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Error response: {"error":"invalid_grant","error_description":"The provided
|
||||
* value for the 'redirect_uri' is not valid. The value must exactly match the
|
||||
* redirect URI used to obtain the authorization
|
||||
* code.","correlation_id":"??????"}
|
||||
*/
|
||||
public static class LiveAuthorizationResponse extends LiveErrorResponse {
|
||||
@SerializedName("token_type")
|
||||
public String tokenType;
|
||||
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("access_token")
|
||||
public String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
public String refreshToken;
|
||||
|
||||
@SerializedName("user_id")
|
||||
public String userId;
|
||||
|
||||
@SerializedName("foci")
|
||||
public String foci;
|
||||
}
|
||||
|
||||
private static class LiveRefreshResponse extends LiveErrorResponse {
|
||||
@SerializedName("expires_in")
|
||||
int expiresIn;
|
||||
|
||||
@SerializedName("access_token")
|
||||
String accessToken;
|
||||
|
||||
@SerializedName("refresh_token")
|
||||
String refreshToken;
|
||||
}
|
||||
|
||||
private static class XBoxLiveAuthenticationResponseDisplayClaims {
|
||||
List<Map<Object, Object>> xui;
|
||||
}
|
||||
@@ -530,44 +420,6 @@ public class MicrosoftService {
|
||||
public String developerMessage;
|
||||
}
|
||||
|
||||
public interface OAuthCallback {
|
||||
/**
|
||||
* Start OAuth callback server at localhost.
|
||||
*
|
||||
* @throws IOException if an I/O error occurred.
|
||||
*/
|
||||
OAuthSession startServer() throws IOException, AuthenticationException;
|
||||
|
||||
/**
|
||||
* Open browser
|
||||
*
|
||||
* @param url OAuth url.
|
||||
*/
|
||||
void openBrowser(String url) throws IOException;
|
||||
|
||||
String getClientId();
|
||||
|
||||
String getClientSecret();
|
||||
}
|
||||
|
||||
public interface OAuthSession {
|
||||
|
||||
String getRedirectURI();
|
||||
|
||||
/**
|
||||
* Wait for authentication
|
||||
*
|
||||
* @return authentication code
|
||||
* @throws InterruptedException if interrupted
|
||||
* @throws ExecutionException if an I/O error occurred.
|
||||
*/
|
||||
String waitFor() throws InterruptedException, ExecutionException;
|
||||
|
||||
default String getIdToken() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static final Gson GSON = new GsonBuilder()
|
||||
.registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE)
|
||||
.registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE)
|
||||
|
||||
@@ -175,7 +175,7 @@ public class YggdrasilServer extends HttpServer {
|
||||
}
|
||||
}
|
||||
if (skin != null && skin.getCape() != null) {
|
||||
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getSkin().getHash())));
|
||||
realTextures.put("CAPE", mapOf(pair("url", rootUrl + "/textures/" + skin.getCape().getHash())));
|
||||
}
|
||||
|
||||
Map<String, Object> textureResponse = mapOf(
|
||||
|
||||
@@ -102,19 +102,27 @@ public class DefaultDependencyManager extends AbstractDependencyManager {
|
||||
if (!gameVersion.isPresent()) return null;
|
||||
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version.resolvePreservingPatches(getGameRepository()));
|
||||
version.resolve(getGameRepository()).getLibraries().stream().filter(Library::appliesToCurrentEnvironment).forEach(library -> {
|
||||
Optional<String> libraryVersion = analyzer.getVersion(OPTIFINE);
|
||||
if (OPTIFINE.matchLibrary(library) && libraryVersion.isPresent()) {
|
||||
if (GameLibrariesTask.shouldDownloadLibrary(repository, version, library, integrityCheck)) {
|
||||
tasks.add(installLibraryAsync(gameVersion.get(), version, OPTIFINE.getPatchId(), libraryVersion.get()));
|
||||
}
|
||||
for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) {
|
||||
Optional<Library> library = analyzer.getLibrary(type);
|
||||
if (library.isPresent() && GameLibrariesTask.shouldDownloadLibrary(repository, version, library.get(), integrityCheck)) {
|
||||
tasks.add(downloadMissingLibraryAsync(gameVersion.get(), version, type, library.get()));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return Task.allOf(tasks);
|
||||
});
|
||||
}
|
||||
|
||||
private Task<?> downloadMissingLibraryAsync(String gameVersion, Version version, LibraryAnalyzer.LibraryType libraryType, Library library) {
|
||||
switch (libraryType) {
|
||||
case OPTIFINE:
|
||||
if (library.hasDownloadURL())
|
||||
break;
|
||||
|
||||
return installLibraryAsync(gameVersion, version, libraryType.getPatchId(), library.getVersion());
|
||||
}
|
||||
return Task.completed(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task<Version> installLibraryAsync(String gameVersion, Version baseVersion, String libraryId, String libraryVersion) {
|
||||
if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved");
|
||||
|
||||
@@ -48,6 +48,10 @@ public final class LibraryAnalyzer implements Iterable<LibraryAnalyzer.LibraryMa
|
||||
return Optional.ofNullable(libraries.get(type)).map(Pair::getValue);
|
||||
}
|
||||
|
||||
public Optional<Library> getLibrary(LibraryType type) {
|
||||
return Optional.ofNullable(libraries.get(type.getPatchId())).map(Pair::getKey);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Iterator<LibraryMark> iterator() {
|
||||
|
||||
@@ -96,7 +96,7 @@ public class RemoteVersion implements Comparable<RemoteVersion> {
|
||||
}
|
||||
|
||||
public Task<Version> getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) {
|
||||
throw new UnsupportedOperationException(toString() + " cannot be installed yet");
|
||||
throw new UnsupportedOperationException(this + " cannot be installed yet");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+22
-1
@@ -20,11 +20,13 @@ package org.jackhuang.hmcl.download.fabric;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.game.Version;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.GetTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -35,6 +37,7 @@ import java.util.List;
|
||||
*/
|
||||
public final class FabricAPIInstallTask extends Task<Version> {
|
||||
|
||||
private GetTask dependent;
|
||||
private final DefaultDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final FabricAPIRemoteVersion remote;
|
||||
@@ -44,6 +47,22 @@ public final class FabricAPIInstallTask extends Task<Version> {
|
||||
this.dependencyManager = dependencyManager;
|
||||
this.version = version;
|
||||
this.remote = remoteVersion;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean doPreExecute() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
this.dependent = new GetTask(new URL(remote.getUrls().get(0) + ".sha1"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<? extends Task<?>> getDependents() {
|
||||
return Collections.singleton(dependent);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -60,6 +79,8 @@ public final class FabricAPIInstallTask extends Task<Version> {
|
||||
public void execute() throws IOException {
|
||||
dependencies.add(new FileDownloadTask(
|
||||
new URL(remote.getUrls().get(0)),
|
||||
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getFullVersion() + ".jar").toFile()));
|
||||
dependencyManager.getGameRepository().getRunDirectory(version.getId()).toPath().resolve("mods").resolve("fabric-api-" + remote.getFullVersion() + ".jar").toFile(),
|
||||
new FileDownloadTask.IntegrityCheck("SHA-1", dependent.getResult().trim()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,10 @@ import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@@ -47,7 +48,7 @@ public final class GameAssetDownloadTask extends Task<Void> {
|
||||
private final AbstractDependencyManager dependencyManager;
|
||||
private final Version version;
|
||||
private final AssetIndexInfo assetIndexInfo;
|
||||
private final File assetIndexFile;
|
||||
private final Path assetIndexFile;
|
||||
private final boolean integrityCheck;
|
||||
private final List<Task<?>> dependents = new LinkedList<>();
|
||||
private final List<Task<?>> dependencies = new LinkedList<>();
|
||||
@@ -93,18 +94,18 @@ public final class GameAssetDownloadTask extends Task<Void> {
|
||||
if (isCancelled())
|
||||
throw new InterruptedException();
|
||||
|
||||
File file = dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject);
|
||||
boolean download = !file.isFile();
|
||||
Path file = dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject);
|
||||
boolean download = !Files.isRegularFile(file);
|
||||
try {
|
||||
if (!download && integrityCheck && !assetObject.validateChecksum(file.toPath(), true))
|
||||
if (!download && integrityCheck && !assetObject.validateChecksum(file, true))
|
||||
download = true;
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + file.toPath(), e);
|
||||
Logging.LOG.log(Level.WARNING, "Unable to calc hash value of file " + file, e);
|
||||
}
|
||||
if (download) {
|
||||
List<URL> urls = dependencyManager.getDownloadProvider().getAssetObjectCandidates(assetObject.getLocation());
|
||||
|
||||
FileDownloadTask task = new FileDownloadTask(urls, file, new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
|
||||
FileDownloadTask task = new FileDownloadTask(urls, file.toFile(), new FileDownloadTask.IntegrityCheck("SHA-1", assetObject.getHash()));
|
||||
task.setName(assetObject.getHash());
|
||||
task.setCandidate(dependencyManager.getCacheRepository().getCommonDirectory()
|
||||
.resolve("assets").resolve("objects").resolve(assetObject.getLocation()));
|
||||
@@ -112,7 +113,7 @@ public final class GameAssetDownloadTask extends Task<Void> {
|
||||
task.setCaching(true);
|
||||
dependencies.add(task.withCounter("hmcl.install.assets"));
|
||||
} else {
|
||||
dependencyManager.getCacheRepository().tryCacheFile(file.toPath(), CacheRepository.SHA1, assetObject.getHash());
|
||||
dependencyManager.getCacheRepository().tryCacheFile(file, CacheRepository.SHA1, assetObject.getHash());
|
||||
}
|
||||
|
||||
updateProgress(++progress, index.getObjects().size());
|
||||
|
||||
+6
-5
@@ -30,8 +30,9 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
@@ -71,14 +72,14 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
|
||||
@Override
|
||||
public void execute() {
|
||||
AssetIndexInfo assetIndexInfo = version.getAssetIndex();
|
||||
File assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||
Path assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId());
|
||||
boolean verifyHashCode = StringUtils.isNotBlank(assetIndexInfo.getSha1()) && assetIndexInfo.getUrl().contains(assetIndexInfo.getSha1());
|
||||
|
||||
if (assetIndexFile.exists() && !forceDownloading) {
|
||||
if (Files.exists(assetIndexFile) && !forceDownloading) {
|
||||
// verify correctness of file content
|
||||
if (verifyHashCode) {
|
||||
try {
|
||||
String actualSum = Hex.encodeHex(DigestUtils.digest("SHA-1", assetIndexFile.toPath()));
|
||||
String actualSum = Hex.encodeHex(DigestUtils.digest("SHA-1", assetIndexFile));
|
||||
if (actualSum.equalsIgnoreCase(assetIndexInfo.getSha1()))
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
@@ -98,7 +99,7 @@ public final class GameAssetIndexDownloadTask extends Task<Void> {
|
||||
// And Mojang will modify this file anytime. So assetIndex.hash might be outdated.
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
dependencyManager.getDownloadProvider().injectURLWithCandidates(assetIndexInfo.getUrl()),
|
||||
assetIndexFile,
|
||||
assetIndexFile.toFile(),
|
||||
verifyHashCode ? new FileDownloadTask.IntegrityCheck("SHA-1", assetIndexInfo.getSha1()) : null
|
||||
);
|
||||
task.setCacheRepository(dependencyManager.getCacheRepository());
|
||||
|
||||
@@ -103,7 +103,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
else
|
||||
throw new LibraryDownloadException(library, t);
|
||||
} else {
|
||||
if (xz) unpackLibrary(jar, Files.readAllBytes(xzFile.toPath()));
|
||||
if (xz) unpackLibrary(jar, FileUtils.readAllBytes(xzFile.toPath()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ public class LibraryDownloadTask extends Task<Void> {
|
||||
if (checksums == null || checksums.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
byte[] fileData = Files.readAllBytes(libPath.toPath());
|
||||
byte[] fileData = FileUtils.readAllBytes(libPath.toPath());
|
||||
boolean valid = checksums.contains(encodeHex(digest("SHA-1", fileData)));
|
||||
if (!valid && libPath.getName().endsWith(".jar")) {
|
||||
valid = validateJar(fileData, checksums);
|
||||
|
||||
@@ -77,7 +77,7 @@ public class JavaDownloadTask extends Task<Void> {
|
||||
|
||||
@Override
|
||||
public void preExecute() throws Exception {
|
||||
this.platform = JavaRepository.getCurrentJavaPlatform().orElseThrow(UnsupportedPlatformException::new);
|
||||
this.platform = JavaRepository.getSystemJavaPlatform().orElseThrow(UnsupportedPlatformException::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -101,7 +101,7 @@ public class JavaDownloadTask extends Task<Void> {
|
||||
try (LZMAInputStream input = new LZMAInputStream(new FileInputStream(tempFile))) {
|
||||
Files.copy(input, dest);
|
||||
} catch (IOException e) {
|
||||
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed");
|
||||
throw new ArtifactMalformedException("File " + entry.getKey() + " is malformed", e);
|
||||
}
|
||||
}));
|
||||
} else if (file.getDownloads().containsKey("raw")) {
|
||||
|
||||
@@ -21,27 +21,29 @@ public final class JavaRepository {
|
||||
private JavaRepository() {
|
||||
}
|
||||
|
||||
public static Task<?> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
public static Task<JavaVersion> downloadJava(GameJavaVersion javaVersion, DownloadProvider downloadProvider) {
|
||||
return new JavaDownloadTask(javaVersion, getJavaStoragePath(), downloadProvider)
|
||||
.thenRunAsync(() -> {
|
||||
Optional<String> platform = getCurrentJavaPlatform();
|
||||
if (platform.isPresent()) {
|
||||
addJava(getJavaHome(javaVersion, platform.get()));
|
||||
}
|
||||
.thenSupplyAsync(() -> {
|
||||
String platform = getSystemJavaPlatform().orElseThrow(JavaDownloadTask.UnsupportedPlatformException::new);
|
||||
return addJava(getJavaHome(javaVersion, platform));
|
||||
});
|
||||
}
|
||||
|
||||
public static void addJava(Path javaHome) throws InterruptedException, IOException {
|
||||
public static JavaVersion addJava(Path javaHome) throws InterruptedException, IOException {
|
||||
if (Files.isDirectory(javaHome)) {
|
||||
Path executable = JavaVersion.getExecutable(javaHome);
|
||||
if (Files.isRegularFile(executable)) {
|
||||
JavaVersion.getJavas().add(JavaVersion.fromExecutable(executable));
|
||||
JavaVersion javaVersion = JavaVersion.fromExecutable(executable);
|
||||
JavaVersion.getJavas().add(javaVersion);
|
||||
return javaVersion;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IOException("Incorrect java home " + javaHome);
|
||||
}
|
||||
|
||||
public static void initialize() throws IOException, InterruptedException {
|
||||
Optional<String> platformOptional = getCurrentJavaPlatform();
|
||||
Optional<String> platformOptional = getSystemJavaPlatform();
|
||||
if (platformOptional.isPresent()) {
|
||||
String platform = platformOptional.get();
|
||||
Path javaStoragePath = getJavaStoragePath();
|
||||
@@ -60,22 +62,28 @@ public final class JavaRepository {
|
||||
}
|
||||
}
|
||||
|
||||
public static Optional<String> getCurrentJavaPlatform() {
|
||||
public static Optional<String> getSystemJavaPlatform() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
if (Architecture.SYSTEM == Architecture.X86) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return Optional.of("linux-i386");
|
||||
} else if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return Optional.of("linux");
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86_64 || Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
return Optional.of("mac-os");
|
||||
}
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (Architecture.SYSTEM == Architecture.X86) {
|
||||
if (Architecture.SYSTEM_ARCH == Architecture.X86) {
|
||||
return Optional.of("windows-x86");
|
||||
} else if (Architecture.SYSTEM == Architecture.X86_64) {
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.X86_64) {
|
||||
return Optional.of("windows-x64");
|
||||
} else if (Architecture.SYSTEM_ARCH == Architecture.ARM64) {
|
||||
if (OperatingSystem.SYSTEM_BUILD_NUMBER >= 21277) {
|
||||
return Optional.of("windows-x64");
|
||||
} else {
|
||||
return Optional.of("windows-x86");
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
|
||||
+1
-1
@@ -230,7 +230,7 @@ public final class OptiFineInstallTask extends Task<Version> {
|
||||
Path configClass = fs.getPath("Config.class");
|
||||
if (!Files.exists(configClass)) configClass = fs.getPath("net/optifine/Config.class");
|
||||
if (!Files.exists(configClass)) throw new IOException("Unrecognized installer");
|
||||
ConstantPool pool = ConstantPoolScanner.parse(Files.readAllBytes(configClass), ConstantType.UTF8);
|
||||
ConstantPool pool = ConstantPoolScanner.parse(FileUtils.readAllBytes(configClass), ConstantType.UTF8);
|
||||
List<String> constants = new ArrayList<>();
|
||||
pool.list(Utf8Constant.class).forEach(utf8 -> constants.add(utf8.get()));
|
||||
String mcVersion = getOrDefault(constants, constants.indexOf("MC_VERSION") + 1, null);
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class EventManager<T extends Event> {
|
||||
return Event.Result.DEFAULT;
|
||||
}
|
||||
|
||||
private synchronized void removeConsumer(Consumer<T> consumer) {
|
||||
public synchronized void unregister(Consumer<T> consumer) {
|
||||
handlers.removeValue(consumer);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ public final class EventManager<T extends Event> {
|
||||
public void accept(T t) {
|
||||
Consumer<T> listener = ref.get();
|
||||
if (listener == null) {
|
||||
removeConsumer(this);
|
||||
unregister(this);
|
||||
} else {
|
||||
listener.accept(t);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,13 @@ public final class CrashReportAnalyzer {
|
||||
// Game crashed when tesselating block model
|
||||
BLOCK(Pattern.compile("Block: (?<type>.*)[\\w\\W\\n\\r]*?Block location: (?<location>.*)"), "type", "location"),
|
||||
// Cannot find native libraries
|
||||
UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name");
|
||||
UNSATISFIED_LINK_ERROR(Pattern.compile("java.lang.UnsatisfiedLinkError: Failed to locate library: (?<name>.*)"), "name"),
|
||||
|
||||
|
||||
|
||||
// Mod issues
|
||||
// TwilightForest is not compatible with OptiFine on Minecraft 1.16.
|
||||
TWILIGHT_FOREST_OPTIFINE(Pattern.compile("java.lang.IllegalArgumentException: (.*) outside of image bounds (.*)"));
|
||||
|
||||
private final Pattern pattern;
|
||||
private final String[] groupNames;
|
||||
@@ -155,8 +161,16 @@ public final class CrashReportAnalyzer {
|
||||
}
|
||||
}
|
||||
|
||||
public static String extractCrashReport(String rawLog) {
|
||||
int begin = rawLog.lastIndexOf("---- Minecraft Crash Report ----");
|
||||
int end = rawLog.lastIndexOf("#@!@# Game crashed! Crash report saved to");
|
||||
if (begin == -1 || end == -1 || begin >= end) return null;
|
||||
return rawLog.substring(begin, end);
|
||||
}
|
||||
|
||||
private static final Pattern CRASH_REPORT_STACK_TRACE_PATTERN = Pattern.compile("Description: (.*?)[\\n\\r]+(?<stacktrace>[\\w\\W\\n\\r]+)A detailed walkthrough of the error");
|
||||
private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((.*?)\\)");
|
||||
private static final Pattern STACK_TRACE_LINE_PATTERN = Pattern.compile("at (?<method>.*?)\\((?<sourcefile>.*?)\\)");
|
||||
private static final Pattern STACK_TRACE_LINE_MODULE_PATTERN = Pattern.compile("\\{(?<tokens>.*)}");
|
||||
private static final Set<String> PACKAGE_KEYWORD_BLACK_LIST = new HashSet<>(Arrays.asList(
|
||||
"net", "minecraft", "item", "block", "player", "tileentity", "events", "common", "client", "entity", "mojang", "main", "gui", "world", "server", "dedicated", // minecraft
|
||||
"renderer", "chunk", "model", "loading", "color", "pipeline", "inventory", "launcher", "physics", "particle", "gen", "registry", "worldgen", "texture", "biomes", "biome",
|
||||
@@ -171,7 +185,7 @@ public final class CrashReportAnalyzer {
|
||||
"fabricmc", "loader", "game", "knot", "launch", "mixin" // fabric
|
||||
));
|
||||
|
||||
public static Set<String> findKeywordsFromCrashReport(String crashReport) throws IOException, InvalidPathException {
|
||||
public static Set<String> findKeywordsFromCrashReport(String crashReport) {
|
||||
Matcher matcher = CRASH_REPORT_STACK_TRACE_PATTERN.matcher(crashReport);
|
||||
Set<String> result = new HashSet<>();
|
||||
if (matcher.find()) {
|
||||
@@ -185,6 +199,20 @@ public final class CrashReportAnalyzer {
|
||||
}
|
||||
result.add(method[i]);
|
||||
}
|
||||
|
||||
Matcher moduleMatcher = STACK_TRACE_LINE_MODULE_PATTERN.matcher(line);
|
||||
if (moduleMatcher.find()) {
|
||||
for (String module : moduleMatcher.group("tokens").split(",")) {
|
||||
String[] split = module.split(":");
|
||||
if (split.length >= 2 && "xf".equals(split[0])) {
|
||||
if (PACKAGE_KEYWORD_BLACK_LIST.contains(split[1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.add(split[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RefreshingVersionsEvent;
|
||||
import org.jackhuang.hmcl.event.RemoveVersionEvent;
|
||||
import org.jackhuang.hmcl.event.RenameVersionEvent;
|
||||
import org.jackhuang.hmcl.game.tlauncher.TLauncherVersion;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.ModpackConfiguration;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.ToStringBuilder;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
@@ -134,6 +136,10 @@ public class DefaultGameRepository implements GameRepository {
|
||||
@Override
|
||||
public File getVersionJar(Version version) {
|
||||
Version v = version.resolve(this);
|
||||
File currentVersionJar = new File(getVersionRoot(v.getId()), v.getId() + ".jar");
|
||||
if (currentVersionJar.exists()) {
|
||||
return currentVersionJar;
|
||||
}
|
||||
String id = Optional.ofNullable(v.getJar()).orElse(v.getId());
|
||||
return new File(getVersionRoot(id), id + ".jar");
|
||||
}
|
||||
@@ -148,14 +154,19 @@ public class DefaultGameRepository implements GameRepository {
|
||||
return gameVersions.get(versionJar);
|
||||
} else {
|
||||
Optional<String> gameVersion = GameVersion.minecraftVersion(versionJar);
|
||||
|
||||
if (!gameVersion.isPresent()) {
|
||||
LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + versionJar.exists());
|
||||
}
|
||||
|
||||
gameVersions.put(versionJar, gameVersion);
|
||||
return gameVersion;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getNativeDirectory(String id) {
|
||||
return new File(getVersionRoot(id), "natives");
|
||||
public File getNativeDirectory(String id, Platform platform) {
|
||||
return new File(getVersionRoot(id), "natives-" + platform);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -172,7 +183,21 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
public Version readVersionJson(File file) throws IOException, JsonParseException {
|
||||
return JsonUtils.fromNonNullJson(FileUtils.readText(file), Version.class);
|
||||
String jsonText = FileUtils.readText(file);
|
||||
try {
|
||||
// Try TLauncher version json format
|
||||
return JsonUtils.fromNonNullJson(jsonText, TLauncherVersion.class).toVersion();
|
||||
} catch (JsonParseException ignored) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Try official version json format
|
||||
return JsonUtils.fromNonNullJson(jsonText, Version.class);
|
||||
} catch (JsonParseException ignored) {
|
||||
}
|
||||
|
||||
LOG.warning("Cannot parse version json + " + file.toString() + "\n" + jsonText);
|
||||
throw new JsonParseException("Version json incorrect");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -191,13 +216,15 @@ public class DefaultGameRepository implements GameRepository {
|
||||
Path toJson = toDir.resolve(to + ".json");
|
||||
Path toJar = toDir.resolve(to + ".jar");
|
||||
|
||||
boolean hasJarFile = Files.exists(fromJar);
|
||||
|
||||
try {
|
||||
Files.move(fromJson, toJson);
|
||||
Files.move(fromJar, toJar);
|
||||
if (hasJarFile) Files.move(fromJar, toJar);
|
||||
} catch (IOException e) {
|
||||
// recovery
|
||||
Lang.ignoringException(() -> Files.move(toJson, fromJson));
|
||||
Lang.ignoringException(() -> Files.move(toJar, fromJar));
|
||||
if (hasJarFile) Lang.ignoringException(() -> Files.move(toJar, fromJar));
|
||||
Lang.ignoringException(() -> Files.move(toDir, fromDir));
|
||||
throw e;
|
||||
}
|
||||
@@ -381,7 +408,7 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getActualAssetDirectory(String version, String assetId) {
|
||||
public Path getActualAssetDirectory(String version, String assetId) {
|
||||
try {
|
||||
return reconstructAssets(version, assetId);
|
||||
} catch (IOException | JsonParseException e) {
|
||||
@@ -391,14 +418,16 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getAssetDirectory(String version, String assetId) {
|
||||
return new File(getBaseDirectory(), "assets");
|
||||
public Path getAssetDirectory(String version, String assetId) {
|
||||
return getBaseDirectory().toPath().resolve("assets");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getAssetObject(String version, String assetId, String name) throws IOException {
|
||||
public Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException {
|
||||
try {
|
||||
return getAssetObject(version, assetId, getAssetIndex(version, assetId).getObjects().get(name));
|
||||
AssetObject assetObject = getAssetIndex(version, assetId).getObjects().get(name);
|
||||
if (assetObject == null) return Optional.empty();
|
||||
return Optional.of(getAssetObject(version, assetId, assetObject));
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
@@ -407,30 +436,30 @@ public class DefaultGameRepository implements GameRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getAssetObject(String version, String assetId, AssetObject obj) {
|
||||
public Path getAssetObject(String version, String assetId, AssetObject obj) {
|
||||
return getAssetObject(version, getAssetDirectory(version, assetId), obj);
|
||||
}
|
||||
|
||||
public File getAssetObject(String version, File assetDir, AssetObject obj) {
|
||||
return new File(assetDir, "objects/" + obj.getLocation());
|
||||
public Path getAssetObject(String version, Path assetDir, AssetObject obj) {
|
||||
return assetDir.resolve("objects").resolve(obj.getLocation());
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getIndexFile(String version, String assetId) {
|
||||
return new File(getAssetDirectory(version, assetId), "indexes/" + assetId + ".json");
|
||||
public Path getIndexFile(String version, String assetId) {
|
||||
return getAssetDirectory(version, assetId).resolve("indexes").resolve(assetId + ".json");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLoggingObject(String version, String assetId, LoggingInfo loggingInfo) {
|
||||
return new File(getAssetDirectory(version, assetId), "log_configs/" + loggingInfo.getFile().getId());
|
||||
public Path getLoggingObject(String version, String assetId, LoggingInfo loggingInfo) {
|
||||
return getAssetDirectory(version, assetId).resolve("log_configs").resolve(loggingInfo.getFile().getId());
|
||||
}
|
||||
|
||||
protected File reconstructAssets(String version, String assetId) throws IOException, JsonParseException {
|
||||
File assetsDir = getAssetDirectory(version, assetId);
|
||||
File indexFile = getIndexFile(version, assetId);
|
||||
File virtualRoot = new File(new File(assetsDir, "virtual"), assetId);
|
||||
protected Path reconstructAssets(String version, String assetId) throws IOException, JsonParseException {
|
||||
Path assetsDir = getAssetDirectory(version, assetId);
|
||||
Path indexFile = getIndexFile(version, assetId);
|
||||
Path virtualRoot = assetsDir.resolve("virtual").resolve(assetId);
|
||||
|
||||
if (!indexFile.isFile())
|
||||
if (!Files.isRegularFile(indexFile))
|
||||
return assetsDir;
|
||||
|
||||
String assetIndexContent = FileUtils.readText(indexFile);
|
||||
@@ -443,11 +472,11 @@ public class DefaultGameRepository implements GameRepository {
|
||||
int cnt = 0;
|
||||
int tot = index.getObjects().entrySet().size();
|
||||
for (Map.Entry<String, AssetObject> entry : index.getObjects().entrySet()) {
|
||||
File target = new File(virtualRoot, entry.getKey());
|
||||
File original = getAssetObject(version, assetsDir, entry.getValue());
|
||||
if (original.exists()) {
|
||||
Path target = virtualRoot.resolve(entry.getKey());
|
||||
Path original = getAssetObject(version, assetsDir, entry.getValue());
|
||||
if (Files.exists(original)) {
|
||||
cnt++;
|
||||
if (!target.isFile())
|
||||
if (!Files.isRegularFile(target))
|
||||
FileUtils.copyFile(original, target);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,4 +37,7 @@ public class GameJavaVersion {
|
||||
public int getMajorVersion() {
|
||||
return majorVersion;
|
||||
}
|
||||
|
||||
public static final GameJavaVersion JAVA_16 = new GameJavaVersion("java-runtime-alpha", 16);
|
||||
public static final GameJavaVersion JAVA_8 = new GameJavaVersion("jre-legacy", 8);
|
||||
}
|
||||
@@ -18,9 +18,11 @@
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -123,9 +125,10 @@ public interface GameRepository extends VersionProvider {
|
||||
* always fail({@code UnsupportedOperationException}) and not to use it.
|
||||
*
|
||||
* @param id version id
|
||||
* @param platform the platform of native libraries
|
||||
* @return the native directory
|
||||
*/
|
||||
File getNativeDirectory(String id);
|
||||
File getNativeDirectory(String id, Platform platform);
|
||||
|
||||
/**
|
||||
* Get minecraft jar
|
||||
@@ -188,7 +191,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
|
||||
* @return the actual asset directory
|
||||
*/
|
||||
File getActualAssetDirectory(String version, String assetId);
|
||||
Path getActualAssetDirectory(String version, String assetId);
|
||||
|
||||
/**
|
||||
* Get the asset directory according to the asset id.
|
||||
@@ -197,7 +200,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
|
||||
* @return the asset directory
|
||||
*/
|
||||
File getAssetDirectory(String version, String assetId);
|
||||
Path getAssetDirectory(String version, String assetId);
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@@ -208,7 +211,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @throws java.io.IOException if I/O operation fails.
|
||||
* @return the file that given asset object refers to
|
||||
*/
|
||||
File getAssetObject(String version, String assetId, String name) throws IOException;
|
||||
Optional<Path> getAssetObject(String version, String assetId, String name) throws IOException;
|
||||
|
||||
/**
|
||||
* Get the file that given asset object refers to
|
||||
@@ -218,7 +221,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @param obj the asset object, you can find it in {@link AssetIndex#getObjects()}
|
||||
* @return the file that given asset object refers to
|
||||
*/
|
||||
File getAssetObject(String version, String assetId, AssetObject obj);
|
||||
Path getAssetObject(String version, String assetId, AssetObject obj);
|
||||
|
||||
/**
|
||||
* Get asset index that assetId represents
|
||||
@@ -235,7 +238,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @param version the id of specific version that is relevant to {@code assetId}
|
||||
* @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()}
|
||||
*/
|
||||
File getIndexFile(String version, String assetId);
|
||||
Path getIndexFile(String version, String assetId);
|
||||
|
||||
/**
|
||||
* Get logging object
|
||||
@@ -245,7 +248,7 @@ public interface GameRepository extends VersionProvider {
|
||||
* @param loggingInfo the logging info
|
||||
* @return the file that loggingInfo refers to
|
||||
*/
|
||||
File getLoggingObject(String version, String assetId, LoggingInfo loggingInfo);
|
||||
Path getLoggingObject(String version, String assetId, LoggingInfo loggingInfo);
|
||||
|
||||
default List<String> getClasspath(Version version) {
|
||||
List<String> classpath = new ArrayList<>();
|
||||
|
||||
@@ -104,13 +104,13 @@ public final class GameVersion {
|
||||
|
||||
Path minecraft = gameJar.getPath("net/minecraft/client/Minecraft.class");
|
||||
if (Files.exists(minecraft)) {
|
||||
Optional<String> result = getVersionOfClassMinecraft(Files.readAllBytes(minecraft));
|
||||
Optional<String> result = getVersionOfClassMinecraft(FileUtils.readAllBytes(minecraft));
|
||||
if (result.isPresent())
|
||||
return result;
|
||||
}
|
||||
Path minecraftServer = gameJar.getPath("net/minecraft/server/MinecraftServer.class");
|
||||
if (Files.exists(minecraftServer))
|
||||
return getVersionFromClassMinecraftServer(Files.readAllBytes(minecraftServer));
|
||||
return getVersionFromClassMinecraftServer(FileUtils.readAllBytes(minecraftServer));
|
||||
return Optional.empty();
|
||||
} catch (IOException e) {
|
||||
return Optional.empty();
|
||||
|
||||
@@ -17,13 +17,16 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.jackhuang.hmcl.util.Range;
|
||||
import org.apache.commons.lang3.Range;
|
||||
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static org.jackhuang.hmcl.download.LibraryAnalyzer.LAUNCH_WRAPPER_MAIN;
|
||||
@@ -36,18 +39,21 @@ public enum JavaVersionConstraint {
|
||||
VANILLA_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, versionRange("1.13", JavaVersionConstraint.MAX), versionRange("1.8", JavaVersionConstraint.MAX)),
|
||||
// Minecraft>=1.7.10+Forge accepts Java 8
|
||||
MODDED_JAVA_8(JavaVersionConstraint.RULE_SUGGESTED, versionRange("1.7.10", JavaVersionConstraint.MAX), versionRange("1.8", JavaVersionConstraint.MAX)),
|
||||
// Minecraft<=1.7.2+Forge requires Java<=7
|
||||
// Minecraft<=1.7.2+Forge requires Java<=7, But LegacyModFixer may fix that problem. So only suggest user using Java 7.
|
||||
MODDED_JAVA_7(JavaVersionConstraint.RULE_SUGGESTED, versionRange(JavaVersionConstraint.MIN, "1.7.2"), versionRange(JavaVersionConstraint.MIN, "1.7.999")) {
|
||||
@Override
|
||||
public boolean appliesToVersion(@Nullable VersionNumber gameVersion, @Nullable Version version) {
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
if (version == null) return false;
|
||||
return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass());
|
||||
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version);
|
||||
return analyzer.has(LibraryAnalyzer.LibraryType.FORGE);
|
||||
}
|
||||
},
|
||||
// LaunchWrapper<=1.12 will crash because of assuming the system class loader is an instance of URLClassLoader (Java 8)
|
||||
// LaunchWrapper<=1.12 will crash because LaunchWrapper assumes the system class loader is an instance of URLClassLoader (Java 8)
|
||||
LAUNCH_WRAPPER(JavaVersionConstraint.RULE_MANDATORY, versionRange("0", "1.12.999"), versionRange("0", "1.8.999")) {
|
||||
@Override
|
||||
public boolean appliesToVersion(VersionNumber gameVersion, Version version) {
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
if (version == null) return false;
|
||||
return LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) &&
|
||||
version.getLibraries().stream()
|
||||
@@ -60,14 +66,15 @@ public enum JavaVersionConstraint {
|
||||
// Minecraft with suggested java version recorded in game json is restrictedly constrained.
|
||||
GAME_JSON(JavaVersionConstraint.RULE_MANDATORY, versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX), versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX)) {
|
||||
@Override
|
||||
public boolean appliesToVersion(VersionNumber gameVersion, Version version) {
|
||||
if (gameVersion == null || version == null) return false;
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
if (version == null) return false;
|
||||
// We only checks for 1.7.10 and above, since 1.7.2 with Forge can only run on Java 7, but it is recorded Java 8 in game json, which is not correct.
|
||||
return gameVersion.compareTo(VersionNumber.asVersion("1.7.10")) >= 0 && version.getJavaVersion() != null;
|
||||
return gameVersionNumber.compareTo(VersionNumber.asVersion("1.7.10")) >= 0 && version.getJavaVersion() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Range<VersionNumber> getJavaVersion(Version version) {
|
||||
public Range<VersionNumber> getJavaVersionRange(Version version) {
|
||||
String javaVersion;
|
||||
if (Objects.requireNonNull(version.getJavaVersion()).getMajorVersion() >= 9) {
|
||||
javaVersion = "" + version.getJavaVersion().getMajorVersion();
|
||||
@@ -81,44 +88,78 @@ public enum JavaVersionConstraint {
|
||||
// For example, JDK 9+ 64-bit cannot load 32-bit lwjgl native library.
|
||||
VANILLA_LINUX_JAVA_8(JavaVersionConstraint.RULE_MANDATORY, versionRange("0", "1.12.999"), versionRange(JavaVersionConstraint.MIN, "1.8.999")) {
|
||||
@Override
|
||||
public boolean appliesToVersion(@Nullable VersionNumber gameVersion, @Nullable Version version) {
|
||||
return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM == Architecture.X86_64;
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX
|
||||
&& Architecture.SYSTEM_ARCH == Architecture.X86_64
|
||||
&& (javaVersion == null || javaVersion.getArchitecture() == Architecture.X86_64);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
@Override
|
||||
public boolean checkJava(VersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
|
||||
return javaVersion.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, javaVersion);
|
||||
}
|
||||
},
|
||||
// Minecraft currently does not provide official support for architectures other than x86 and x86-64.
|
||||
VANILLA_X86(JavaVersionConstraint.RULE_MANDATORY, versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX), versionRange(JavaVersionConstraint.MIN, JavaVersionConstraint.MAX)) {
|
||||
@Override
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
return javaVersion != null && !javaVersion.getArchitecture().isX86();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkJava(VersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
|
||||
return javaVersion.getArchitecture().isX86();
|
||||
}
|
||||
};
|
||||
|
||||
private final int type;
|
||||
private final Range<VersionNumber> gameVersion;
|
||||
private final Range<VersionNumber> javaVersion;
|
||||
private final Range<VersionNumber> gameVersionRange;
|
||||
private final Range<VersionNumber> javaVersionRange;
|
||||
|
||||
JavaVersionConstraint(int type, Range<VersionNumber> gameVersion, Range<VersionNumber> javaVersion) {
|
||||
JavaVersionConstraint(int type, Range<VersionNumber> gameVersionRange, Range<VersionNumber> javaVersionRange) {
|
||||
this.type = type;
|
||||
this.gameVersion = gameVersion;
|
||||
this.javaVersion = javaVersion;
|
||||
this.gameVersionRange = gameVersionRange;
|
||||
this.javaVersionRange = javaVersionRange;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Range<VersionNumber> getGameVersion() {
|
||||
return gameVersion;
|
||||
public Range<VersionNumber> getGameVersionRange() {
|
||||
return gameVersionRange;
|
||||
}
|
||||
|
||||
public Range<VersionNumber> getJavaVersion(Version version) {
|
||||
return javaVersion;
|
||||
public Range<VersionNumber> getJavaVersionRange(Version version) {
|
||||
return javaVersionRange;
|
||||
}
|
||||
|
||||
public boolean appliesToVersion(@Nullable VersionNumber gameVersion, @Nullable Version version) {
|
||||
public final boolean appliesToVersion(@Nullable VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
return gameVersionRange.contains(gameVersionNumber)
|
||||
&& appliesToVersionImpl(gameVersionNumber, version, javaVersion);
|
||||
}
|
||||
|
||||
protected boolean appliesToVersionImpl(VersionNumber gameVersionNumber, @Nullable Version version,
|
||||
@Nullable JavaVersion javaVersion) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static VersionRange findSuitableJavaVersionRange(VersionNumber gameVersion, Version version) {
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
public boolean checkJava(VersionNumber gameVersionNumber, Version version, JavaVersion javaVersion) {
|
||||
return getJavaVersionRange(version).contains(javaVersion.getVersionNumber());
|
||||
}
|
||||
|
||||
public static final List<JavaVersionConstraint> ALL = Lang.immutableListOf(values());
|
||||
|
||||
public static VersionRanges findSuitableJavaVersionRange(VersionNumber gameVersion, Version version) {
|
||||
Range<VersionNumber> mandatoryJavaRange = versionRange(MIN, MAX);
|
||||
Range<VersionNumber> suggestedJavaRange = versionRange(MIN, MAX);
|
||||
for (JavaVersionConstraint java : values()) {
|
||||
if (java.gameVersion.contains(gameVersion) && java.appliesToVersion(gameVersion, version)) {
|
||||
Range<VersionNumber> javaVersionRange = java.getJavaVersion(version);
|
||||
for (JavaVersionConstraint java : ALL) {
|
||||
if (java.appliesToVersion(gameVersion, version, null)) {
|
||||
Range<VersionNumber> javaVersionRange = java.getJavaVersionRange(version);
|
||||
if (java.type == RULE_MANDATORY) {
|
||||
mandatoryJavaRange = mandatoryJavaRange.intersectionWith(javaVersionRange);
|
||||
suggestedJavaRange = suggestedJavaRange.intersectionWith(javaVersionRange);
|
||||
@@ -127,26 +168,30 @@ public enum JavaVersionConstraint {
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VersionRange(mandatoryJavaRange, suggestedJavaRange);
|
||||
return new VersionRanges(mandatoryJavaRange, suggestedJavaRange);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static JavaVersion findSuitableJavaVersion(VersionNumber gameVersion, Version version) throws InterruptedException {
|
||||
VersionRange range = findSuitableJavaVersionRange(gameVersion, version);
|
||||
VersionRanges range = findSuitableJavaVersionRange(gameVersion, version);
|
||||
|
||||
JavaVersion mandatory = null;
|
||||
JavaVersion suggested = null;
|
||||
for (JavaVersion javaVersion : JavaVersion.getJavas()) {
|
||||
// select the latest java version that this version accepts.
|
||||
if (range.getMandatory().contains(javaVersion.getVersionNumber())) {
|
||||
// select the latest x86 java that this version accepts.
|
||||
if (!javaVersion.getArchitecture().isX86())
|
||||
continue;
|
||||
|
||||
VersionNumber javaVersionNumber = javaVersion.getVersionNumber();
|
||||
if (range.getMandatory().contains(javaVersionNumber)) {
|
||||
if (mandatory == null) mandatory = javaVersion;
|
||||
else if (javaVersion.getVersionNumber().compareTo(mandatory.getVersionNumber()) > 0) {
|
||||
else if (compareJavaVersion(javaVersion, mandatory) > 0) {
|
||||
mandatory = javaVersion;
|
||||
}
|
||||
}
|
||||
if (range.getSuggested().contains(javaVersion.getVersionNumber())) {
|
||||
if (range.getSuggested().contains(javaVersionNumber)) {
|
||||
if (suggested == null) suggested = javaVersion;
|
||||
else if (javaVersion.getVersionNumber().compareTo(suggested.getVersionNumber()) > 0) {
|
||||
else if (compareJavaVersion(javaVersion, suggested) > 0) {
|
||||
suggested = javaVersion;
|
||||
}
|
||||
}
|
||||
@@ -156,6 +201,21 @@ public enum JavaVersionConstraint {
|
||||
else return mandatory;
|
||||
}
|
||||
|
||||
private static int compareJavaVersion(JavaVersion javaVersion1, JavaVersion javaVersion2) {
|
||||
Architecture arch1 = javaVersion1.getArchitecture();
|
||||
Architecture arch2 = javaVersion2.getArchitecture();
|
||||
|
||||
if (arch1 != arch2) {
|
||||
if (arch1 == Architecture.X86_64) {
|
||||
return 1;
|
||||
}
|
||||
if (arch2 == Architecture.X86_64) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return javaVersion1.getVersionNumber().compareTo(javaVersion2.getVersionNumber());
|
||||
}
|
||||
|
||||
public static final int RULE_MANDATORY = 1;
|
||||
public static final int RULE_SUGGESTED = 2;
|
||||
|
||||
@@ -166,11 +226,11 @@ public enum JavaVersionConstraint {
|
||||
return Range.between(VersionNumber.asVersion(fromInclusive), VersionNumber.asVersion(toExclusive));
|
||||
}
|
||||
|
||||
public static class VersionRange {
|
||||
public static class VersionRanges {
|
||||
private final Range<VersionNumber> mandatory;
|
||||
private final Range<VersionNumber> suggested;
|
||||
|
||||
public VersionRange(Range<VersionNumber> mandatory, Range<VersionNumber> suggested) {
|
||||
public VersionRanges(Range<VersionNumber> mandatory, Range<VersionNumber> suggested) {
|
||||
this.mandatory = mandatory;
|
||||
this.suggested = suggested;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 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
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.util.Constants;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
@@ -26,6 +26,7 @@ import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -94,7 +95,7 @@ public class Library implements Comparable<Library>, Validation {
|
||||
public String getClassifier() {
|
||||
if (artifact.getClassifier() == null)
|
||||
if (natives != null && natives.containsKey(OperatingSystem.CURRENT_OS))
|
||||
return natives.get(OperatingSystem.CURRENT_OS).replace("${arch}", Architecture.SYSTEM.getPlatform().getBit());
|
||||
return natives.get(OperatingSystem.CURRENT_OS).replace("${arch}", Architecture.SYSTEM_ARCH.getBits().getBit());
|
||||
else
|
||||
return null;
|
||||
else
|
||||
@@ -142,6 +143,12 @@ public class Library implements Comparable<Library>, Validation {
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasDownloadURL() {
|
||||
LibraryDownloadInfo temp = getRawDownloadInfo();
|
||||
if (temp != null) return temp.getUrl() != null;
|
||||
else return url != null;
|
||||
}
|
||||
|
||||
public List<String> getChecksums() {
|
||||
return checksums;
|
||||
}
|
||||
@@ -154,6 +161,7 @@ public class Library implements Comparable<Library>, Validation {
|
||||
* Hint for how to locate the library file.
|
||||
* @return null for default, "local" for location in version/<version>/libraries/filename
|
||||
*/
|
||||
@Nullable
|
||||
public String getHint() {
|
||||
return hint;
|
||||
}
|
||||
@@ -162,6 +170,7 @@ public class Library implements Comparable<Library>, Validation {
|
||||
* Available when hint is "local"
|
||||
* @return the filename of the local library in version/<version>/libraries/$filename
|
||||
*/
|
||||
@Nullable
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ public final class OSRestriction {
|
||||
return false;
|
||||
|
||||
if (arch != null)
|
||||
return !Lang.test(() -> !Pattern.compile(arch).matcher(Architecture.SYSTEM_ARCHITECTURE).matches());
|
||||
return !Lang.test(() -> !Pattern.compile(arch).matcher(Architecture.SYSTEM_ARCH.getCheckedName()).matches());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -85,13 +85,28 @@ public class RuledArgument implements Argument {
|
||||
@Override
|
||||
public RuledArgument deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
JsonObject obj = json.getAsJsonObject();
|
||||
return new RuledArgument(
|
||||
context.deserialize(obj.get("rules"), new TypeToken<List<CompatibilityRule>>() {
|
||||
}.getType()),
|
||||
obj.get("value").isJsonPrimitive()
|
||||
? Collections.singletonList(obj.get("value").getAsString())
|
||||
: context.deserialize(obj.get("value"), new TypeToken<List<String>>() {
|
||||
}.getType()));
|
||||
|
||||
List<CompatibilityRule> rules = context.deserialize(obj.get("rules"), new TypeToken<List<CompatibilityRule>>() {
|
||||
}.getType());
|
||||
|
||||
JsonElement valuesElement;
|
||||
if (obj.has("values")) {
|
||||
valuesElement = obj.get("values");
|
||||
} else if (obj.has("value")) {
|
||||
valuesElement = obj.get("value");
|
||||
} else {
|
||||
throw new JsonParseException("RuledArguments instance does not have either value or values member.");
|
||||
}
|
||||
|
||||
List<String> values;
|
||||
if (valuesElement.isJsonPrimitive()) {
|
||||
values = Collections.singletonList(valuesElement.getAsString());
|
||||
} else {
|
||||
values = context.deserialize(valuesElement, new TypeToken<List<String>>() {
|
||||
}.getType());
|
||||
}
|
||||
|
||||
return new RuledArgument(rules, values);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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;
|
||||
|
||||
public enum VersionJsonType {
|
||||
OFFICIAL,
|
||||
TLAUNCHER
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.tlauncher;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Immutable
|
||||
public class TLauncherLibrary {
|
||||
|
||||
@SerializedName("name")
|
||||
private final Artifact name;
|
||||
private final String url;
|
||||
private final LibraryDownloadInfo artifact;
|
||||
|
||||
@SerializedName("classifies") // stupid typo made by TLauncher
|
||||
private final Map<String, LibraryDownloadInfo> classifiers;
|
||||
private final ExtractRules extract;
|
||||
private final Map<OperatingSystem, String> natives;
|
||||
private final List<CompatibilityRule> rules;
|
||||
private final List<String> checksums;
|
||||
|
||||
public TLauncherLibrary(Artifact name, String url, LibraryDownloadInfo artifact, Map<String, LibraryDownloadInfo> classifiers, ExtractRules extract, Map<OperatingSystem, String> natives, List<CompatibilityRule> rules, List<String> checksums) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.artifact = artifact;
|
||||
this.classifiers = classifiers;
|
||||
this.extract = extract;
|
||||
this.natives = natives;
|
||||
this.rules = rules;
|
||||
this.checksums = checksums;
|
||||
}
|
||||
|
||||
public Library toLibrary() {
|
||||
return new Library(
|
||||
name,
|
||||
url,
|
||||
new LibrariesDownloadInfo(artifact, classifiers),
|
||||
checksums,
|
||||
extract,
|
||||
natives,
|
||||
rules,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.tlauncher;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.util.gson.JsonMap;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TLauncherVersion implements Validation {
|
||||
|
||||
private final String id;
|
||||
private final String minecraftArguments;
|
||||
private final Arguments arguments;
|
||||
private final String mainClass;
|
||||
private final String inheritsFrom;
|
||||
private final String jar;
|
||||
private final AssetIndexInfo assetIndex;
|
||||
private final String assets;
|
||||
private final Integer complianceLevel;
|
||||
@Nullable
|
||||
private final GameJavaVersion javaVersion;
|
||||
private final List<TLauncherLibrary> libraries;
|
||||
private final List<CompatibilityRule> compatibilityRules;
|
||||
private final JsonMap<DownloadType, DownloadInfo> downloads;
|
||||
private final JsonMap<DownloadType, LoggingInfo> logging;
|
||||
private final ReleaseType type;
|
||||
private final Date time;
|
||||
private final Date releaseTime;
|
||||
private final Integer minimumLauncherVersion;
|
||||
private final Integer tlauncherVersion;
|
||||
|
||||
public TLauncherVersion(String id, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, Integer complianceLevel, @Nullable GameJavaVersion javaVersion, List<TLauncherLibrary> libraries, List<CompatibilityRule> compatibilityRules, JsonMap<DownloadType, DownloadInfo> downloads, JsonMap<DownloadType, LoggingInfo> logging, ReleaseType type, Date time, Date releaseTime, Integer minimumLauncherVersion, Integer tlauncherVersion) {
|
||||
this.id = id;
|
||||
this.minecraftArguments = minecraftArguments;
|
||||
this.arguments = arguments;
|
||||
this.mainClass = mainClass;
|
||||
this.inheritsFrom = inheritsFrom;
|
||||
this.jar = jar;
|
||||
this.assetIndex = assetIndex;
|
||||
this.assets = assets;
|
||||
this.complianceLevel = complianceLevel;
|
||||
this.javaVersion = javaVersion;
|
||||
this.libraries = libraries;
|
||||
this.compatibilityRules = compatibilityRules;
|
||||
this.downloads = downloads;
|
||||
this.logging = logging;
|
||||
this.type = type;
|
||||
this.time = time;
|
||||
this.releaseTime = releaseTime;
|
||||
this.minimumLauncherVersion = minimumLauncherVersion;
|
||||
this.tlauncherVersion = tlauncherVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate() throws JsonParseException, TolerableValidationException {
|
||||
Validation.requireNonNull(tlauncherVersion, "Not TLauncher version json format");
|
||||
}
|
||||
|
||||
public Version toVersion() {
|
||||
return new Version(
|
||||
false,
|
||||
id,
|
||||
null,
|
||||
null,
|
||||
minecraftArguments,
|
||||
arguments,
|
||||
mainClass,
|
||||
inheritsFrom,
|
||||
jar,
|
||||
assetIndex,
|
||||
assets,
|
||||
complianceLevel,
|
||||
javaVersion,
|
||||
libraries == null ? null : libraries.stream().map(TLauncherLibrary::toLibrary).collect(Collectors.toList()),
|
||||
compatibilityRules,
|
||||
downloads,
|
||||
logging,
|
||||
type,
|
||||
time,
|
||||
releaseTime,
|
||||
minimumLauncherVersion,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.launch;
|
||||
|
||||
public class CommandTooLongException extends RuntimeException {
|
||||
public CommandTooLongException() {
|
||||
}
|
||||
|
||||
public CommandTooLongException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CommandTooLongException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public CommandTooLongException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -36,16 +36,15 @@ import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
import org.jackhuang.hmcl.util.platform.Bits;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.*;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Proxy;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -122,7 +121,10 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
res.addDefault("-Xdock:name=", "Minecraft " + version.getId());
|
||||
res.addDefault("-Xdock:icon=", repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns").getAbsolutePath());
|
||||
repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns")
|
||||
.ifPresent(minecraftIcns -> {
|
||||
res.addDefault("-Xdock:icon=", minecraftIcns.toAbsolutePath().toString());
|
||||
});
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS)
|
||||
@@ -160,7 +162,7 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
// As 32-bit JVM allocate 320KB for stack by default rather than 64-bit version allocating 1MB,
|
||||
// causing Minecraft 1.13 crashed accounting for java.lang.StackOverflowError.
|
||||
if (options.getJava().getPlatform() == Platform.BIT_32) {
|
||||
if (options.getJava().getBits() == Bits.BIT_32) {
|
||||
res.addDefault("-Xss", "1m");
|
||||
}
|
||||
|
||||
@@ -203,12 +205,12 @@ public class DefaultLauncher extends Launcher {
|
||||
classpath.add(jar.getAbsolutePath());
|
||||
|
||||
// Provided Minecraft arguments
|
||||
File gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId());
|
||||
Path gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId());
|
||||
Map<String, String> configuration = getConfigurations();
|
||||
configuration.put("${classpath}", String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
||||
configuration.put("${natives_directory}", nativeFolder.getAbsolutePath());
|
||||
configuration.put("${game_assets}", gameAssets.getAbsolutePath());
|
||||
configuration.put("${assets_root}", gameAssets.getAbsolutePath());
|
||||
configuration.put("${game_assets}", gameAssets.toAbsolutePath().toString());
|
||||
configuration.put("${assets_root}", gameAssets.toAbsolutePath().toString());
|
||||
|
||||
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
|
||||
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
|
||||
@@ -354,15 +356,23 @@ public class DefaultLauncher extends Launcher {
|
||||
|
||||
@Override
|
||||
public ManagedProcess launch() throws IOException, InterruptedException {
|
||||
File nativeFolder = null;
|
||||
File nativeFolder;
|
||||
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
nativeFolder = repository.getNativeDirectory(version.getId());
|
||||
nativeFolder = repository.getNativeDirectory(version.getId(), options.getJava().getPlatform());
|
||||
} else {
|
||||
nativeFolder = new File(options.getNativesDir());
|
||||
}
|
||||
|
||||
// To guarantee that when failed to generate launch command line, we will not call pre-launch command
|
||||
List<String> rawCommandLine = generateCommandLine(nativeFolder).asList();
|
||||
List<String> rawCommandLine = generateCommandLine(nativeFolder).asMutableList();
|
||||
|
||||
// Pass classpath using the environment variable, to reduce the command length
|
||||
String classpath = null;
|
||||
final int cpIndex = rawCommandLine.indexOf("-cp");
|
||||
if (cpIndex >= 0 && cpIndex < rawCommandLine.size() - 1) {
|
||||
rawCommandLine.remove(cpIndex); // remove "-cp"
|
||||
classpath = rawCommandLine.remove(cpIndex);
|
||||
}
|
||||
|
||||
if (rawCommandLine.stream().anyMatch(StringUtils::isBlank)) {
|
||||
throw new IllegalStateException("Illegal command line " + rawCommandLine);
|
||||
@@ -388,13 +398,14 @@ public class DefaultLauncher extends Launcher {
|
||||
}
|
||||
String appdata = options.getGameDir().getAbsoluteFile().getParent();
|
||||
if (appdata != null) builder.environment().put("APPDATA", appdata);
|
||||
if (classpath != null) builder.environment().put("CLASSPATH", classpath);
|
||||
builder.environment().putAll(getEnvVars());
|
||||
process = builder.start();
|
||||
} catch (IOException e) {
|
||||
throw new ProcessCreationException(e);
|
||||
}
|
||||
|
||||
ManagedProcess p = new ManagedProcess(process, rawCommandLine);
|
||||
ManagedProcess p = new ManagedProcess(process, rawCommandLine, classpath);
|
||||
if (listener != null)
|
||||
startMonitors(p, listener, daemon);
|
||||
return p;
|
||||
@@ -429,9 +440,9 @@ public class DefaultLauncher extends Launcher {
|
||||
public void makeLaunchScript(File scriptFile) throws IOException {
|
||||
boolean isWindows = OperatingSystem.WINDOWS == OperatingSystem.CURRENT_OS;
|
||||
|
||||
File nativeFolder = null;
|
||||
File nativeFolder;
|
||||
if (options.getNativesDirType() == NativesDirectoryType.VERSION_FOLDER) {
|
||||
nativeFolder = repository.getNativeDirectory(version.getId());
|
||||
nativeFolder = repository.getNativeDirectory(version.getId(), options.getJava().getPlatform());
|
||||
} else {
|
||||
nativeFolder = new File(options.getNativesDir());
|
||||
}
|
||||
@@ -440,54 +451,113 @@ public class DefaultLauncher extends Launcher {
|
||||
decompressNatives(nativeFolder);
|
||||
}
|
||||
|
||||
if (isWindows && !FileUtils.getExtension(scriptFile).equals("bat"))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' in Windows");
|
||||
else if (!isWindows && !FileUtils.getExtension(scriptFile).equals("sh"))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh' in macOS/Linux");
|
||||
String scriptExtension = FileUtils.getExtension(scriptFile);
|
||||
boolean usePowerShell = "ps1".equals(scriptExtension);
|
||||
|
||||
if (!usePowerShell) {
|
||||
if (isWindows && !scriptExtension.equals("bat"))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'bat' or 'ps1' in Windows");
|
||||
else if (!isWindows && !scriptExtension.equals("sh"))
|
||||
throw new IllegalArgumentException("The extension of " + scriptFile + " is not 'sh' or 'ps1' in macOS/Linux");
|
||||
}
|
||||
|
||||
if (!FileUtils.makeFile(scriptFile))
|
||||
throw new IOException("Script file: " + scriptFile + " cannot be created.");
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(scriptFile)))) {
|
||||
if (isWindows) {
|
||||
writer.write("@echo off");
|
||||
writer.newLine();
|
||||
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
|
||||
writer.newLine();
|
||||
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
|
||||
writer.write("set " + entry.getKey() + "=" + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.newLine();
|
||||
writer.write(new CommandBuilder().add("cd", "/D", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
|
||||
writer.newLine();
|
||||
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX || OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
writer.write("#!/usr/bin/env bash");
|
||||
writer.newLine();
|
||||
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
|
||||
writer.write("export " + entry.getKey() + "=" + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
|
||||
writer.newLine();
|
||||
}
|
||||
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
|
||||
writer.write(options.getPreLaunchCommand());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write(generateCommandLine(nativeFolder).toString());
|
||||
writer.newLine();
|
||||
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
|
||||
writer.write(options.getPostExitCommand());
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
writer.write("pause");
|
||||
final CommandBuilder commandLine = generateCommandLine(nativeFolder);
|
||||
final String command = usePowerShell ? null : commandLine.toString();
|
||||
|
||||
if (!usePowerShell && isWindows) {
|
||||
if (command.length() > 8192) { // maximum length of the command in cmd
|
||||
throw new CommandTooLongException();
|
||||
}
|
||||
}
|
||||
|
||||
OutputStream outputStream = new FileOutputStream(scriptFile);
|
||||
Charset charset = StandardCharsets.UTF_8;
|
||||
|
||||
if (isWindows) {
|
||||
if (usePowerShell) {
|
||||
// Write UTF-8 BOM
|
||||
try {
|
||||
outputStream.write(0xEF);
|
||||
outputStream.write(0xBB);
|
||||
outputStream.write(0xBF);
|
||||
} catch (IOException e) {
|
||||
outputStream.close();
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
charset = OperatingSystem.NATIVE_CHARSET;
|
||||
}
|
||||
}
|
||||
|
||||
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream, charset))) {
|
||||
if (usePowerShell) {
|
||||
if (isWindows) {
|
||||
writer.write("$Env:APPDATA=");
|
||||
writer.write(CommandBuilder.pwshString(options.getGameDir().getAbsoluteFile().getParent()));
|
||||
writer.newLine();
|
||||
}
|
||||
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
|
||||
writer.write("$Env:" + entry.getKey() + "=");
|
||||
writer.write(CommandBuilder.pwshString(entry.getValue()));
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write("Set-Location -Path ");
|
||||
writer.write(CommandBuilder.pwshString(repository.getRunDirectory(version.getId()).getAbsolutePath()));
|
||||
writer.newLine();
|
||||
|
||||
writer.write('&');
|
||||
for (String rawCommand : commandLine.asList()) {
|
||||
writer.write(' ');
|
||||
writer.write(CommandBuilder.pwshString(rawCommand));
|
||||
}
|
||||
writer.newLine();
|
||||
} else {
|
||||
if (isWindows) {
|
||||
writer.write("@echo off");
|
||||
writer.newLine();
|
||||
writer.write("set APPDATA=" + options.getGameDir().getAbsoluteFile().getParent());
|
||||
writer.newLine();
|
||||
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
|
||||
writer.write("set " + entry.getKey() + "=" + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.newLine();
|
||||
writer.write(new CommandBuilder().add("cd", "/D", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
|
||||
} else {
|
||||
writer.write("#!/usr/bin/env bash");
|
||||
writer.newLine();
|
||||
for (Map.Entry<String, String> entry : getEnvVars().entrySet()) {
|
||||
writer.write("export " + entry.getKey() + "=" + entry.getValue());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write(new CommandBuilder().add("cd", repository.getRunDirectory(version.getId()).getAbsolutePath()).toString());
|
||||
}
|
||||
writer.newLine();
|
||||
if (StringUtils.isNotBlank(options.getPreLaunchCommand())) {
|
||||
writer.write(options.getPreLaunchCommand());
|
||||
writer.newLine();
|
||||
}
|
||||
writer.write(generateCommandLine(nativeFolder).toString());
|
||||
writer.newLine();
|
||||
if (StringUtils.isNotBlank(options.getPostExitCommand())) {
|
||||
writer.write(options.getPostExitCommand());
|
||||
writer.newLine();
|
||||
}
|
||||
|
||||
if (isWindows) {
|
||||
writer.write("pause");
|
||||
writer.newLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!scriptFile.setExecutable(true))
|
||||
throw new PermissionException();
|
||||
|
||||
if (usePowerShell && !CommandBuilder.hasExecutionPolicy())
|
||||
throw new ExecutionPolicyLimitException();
|
||||
}
|
||||
|
||||
private void startMonitors(ManagedProcess managedProcess, ProcessListener processListener, boolean isDaemon) {
|
||||
@@ -495,12 +565,12 @@ public class DefaultLauncher extends Launcher {
|
||||
Thread stdout = Lang.thread(new StreamPump(managedProcess.getProcess().getInputStream(), it -> {
|
||||
processListener.onLog(it, Optional.ofNullable(Log4jLevel.guessLevel(it)).orElse(Log4jLevel.INFO));
|
||||
managedProcess.addLine(it);
|
||||
}), "stdout-pump", isDaemon);
|
||||
}, OperatingSystem.NATIVE_CHARSET), "stdout-pump", isDaemon);
|
||||
managedProcess.addRelatedThread(stdout);
|
||||
Thread stderr = Lang.thread(new StreamPump(managedProcess.getProcess().getErrorStream(), it -> {
|
||||
processListener.onLog(it, Log4jLevel.ERROR);
|
||||
managedProcess.addLine(it);
|
||||
}), "stderr-pump", isDaemon);
|
||||
}, OperatingSystem.NATIVE_CHARSET), "stderr-pump", isDaemon);
|
||||
managedProcess.addRelatedThread(stderr);
|
||||
managedProcess.addRelatedThread(Lang.thread(new ExitWaiter(managedProcess, Arrays.asList(stdout, stderr), processListener::onExit), "exit-waiter", isDaemon));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.jackhuang.hmcl.launch;
|
||||
|
||||
public final class ExecutionPolicyLimitException extends RuntimeException {
|
||||
public ExecutionPolicyLimitException() {
|
||||
}
|
||||
|
||||
public ExecutionPolicyLimitException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ExecutionPolicyLimitException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public ExecutionPolicyLimitException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -36,20 +37,27 @@ public final class StreamPump implements Runnable {
|
||||
|
||||
private final InputStream inputStream;
|
||||
private final Consumer<String> callback;
|
||||
private final Charset charset;
|
||||
|
||||
public StreamPump(InputStream inputStream) {
|
||||
this(inputStream, s -> {
|
||||
});
|
||||
this(inputStream, s -> {});
|
||||
}
|
||||
|
||||
public StreamPump(InputStream inputStream, Consumer<String> callback) {
|
||||
this.inputStream = inputStream;
|
||||
this.callback = callback;
|
||||
this.charset = StandardCharsets.UTF_8;
|
||||
}
|
||||
|
||||
public StreamPump(InputStream inputStream, Consumer<String> callback, Charset charset) {
|
||||
this.inputStream = inputStream;
|
||||
this.callback = callback;
|
||||
this.charset = charset;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, Charset.defaultCharset()))) {
|
||||
try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, charset))) {
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
if (Thread.currentThread().isInterrupted()) {
|
||||
|
||||
@@ -23,13 +23,14 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Immutable;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -121,11 +122,11 @@ public final class CurseManifest {
|
||||
* @throws JsonParseException if the manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
*/
|
||||
public static Modpack readCurseForgeModpackManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "manifest.json", encoding);
|
||||
public static Modpack readCurseForgeModpackManifest(ZipFileSystem zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = FileUtils.readText(zip.getPath("manifest.json"));
|
||||
CurseManifest manifest = JsonUtils.fromNonNullJson(json, CurseManifest.class);
|
||||
return new Modpack(manifest.getName(), manifest.getAuthor(), manifest.getVersion(), manifest.getMinecraft().getGameVersion(),
|
||||
CompressingUtils.readTextZipEntryQuietly(zip, "modlist.html", encoding).orElse( "No description"), encoding, manifest) {
|
||||
Lang.ignoringException(() -> FileUtils.readText(zip.getPath("modlist.html")), "No description"), encoding, manifest) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name);
|
||||
|
||||
@@ -25,7 +25,6 @@ import org.jackhuang.hmcl.game.Library;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.*;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
@@ -33,7 +32,6 @@ import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -435,29 +433,26 @@ public class McbbsModpackManifest implements Validation {
|
||||
}
|
||||
|
||||
private static Modpack fromManifestFile(Path manifestFile, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = FileUtils.readText(manifestFile, StandardCharsets.UTF_8);
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(json, McbbsModpackManifest.class);
|
||||
McbbsModpackManifest manifest = JsonUtils.fromNonNullJson(FileUtils.readText(manifestFile), McbbsModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param zip the MCBBS modpack file.
|
||||
* @param fs the MCBBS modpack file.
|
||||
* @param encoding the modpack zip file encoding.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
* @return the manifest.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zip, encoding)) {
|
||||
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
|
||||
if (Files.exists(mcbbsPackMeta)) {
|
||||
return fromManifestFile(mcbbsPackMeta, encoding);
|
||||
}
|
||||
Path manifestJson = fs.getPath("manifest.json");
|
||||
if (Files.exists(manifestJson)) {
|
||||
return fromManifestFile(manifestJson, encoding);
|
||||
}
|
||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
Path mcbbsPackMeta = fs.getPath("mcbbs.packmeta");
|
||||
if (Files.exists(mcbbsPackMeta)) {
|
||||
return fromManifestFile(mcbbsPackMeta, encoding);
|
||||
}
|
||||
Path manifestJson = fs.getPath("manifest.json");
|
||||
if (Files.exists(manifestJson)) {
|
||||
return fromManifestFile(manifestJson, encoding);
|
||||
}
|
||||
throw new IOException("`mcbbs.packmeta` or `manifest.json` cannot be found");
|
||||
}
|
||||
}
|
||||
+17
-20
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
* Copyright (C) 2021 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
|
||||
@@ -21,8 +21,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
import org.jackhuang.hmcl.mod.Modpack;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -30,7 +30,6 @@ import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
@@ -348,24 +347,22 @@ public final class MultiMCInstanceConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
public static Modpack readMultiMCModpackManifest(Path modpackFile, Charset encoding) throws IOException {
|
||||
try (FileSystem fs = CompressingUtils.readonly(modpackFile).setEncoding(encoding).build()) {
|
||||
Path root = getRootPath(fs.getPath("/"));
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
|
||||
String name = FileUtils.getName(root, FileUtils.getNameWithoutExtension(modpackFile));
|
||||
public static Modpack readMultiMCModpackManifest(ZipFileSystem zipFile, Path filePath, Charset encoding) throws IOException {
|
||||
Path root = getRootPath(zipFile.getPath("/"));
|
||||
MultiMCManifest manifest = MultiMCManifest.readMultiMCModpackManifest(root);
|
||||
String name = FileUtils.getNameWithoutExtension(filePath);
|
||||
|
||||
Path instancePath = root.resolve("instance.cfg");
|
||||
if (Files.notExists(instancePath))
|
||||
throw new IOException("`instance.cfg` not found, " + modpackFile + " is not a valid MultiMC modpack.");
|
||||
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
|
||||
}
|
||||
};
|
||||
}
|
||||
Path instancePath = root.resolve("instance.cfg");
|
||||
if (Files.notExists(instancePath))
|
||||
throw new IOException("`instance.cfg` not found, " + filePath + " is not a valid MultiMC modpack.");
|
||||
try (InputStream instanceStream = Files.newInputStream(instancePath)) {
|
||||
MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest);
|
||||
return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) {
|
||||
@Override
|
||||
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, File zipFile, String name) {
|
||||
return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -32,6 +32,7 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.ZipFileSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -144,11 +145,11 @@ public final class MultiMCModpackInstallTask extends Task<Void> {
|
||||
public void execute() throws Exception {
|
||||
Version version = repository.readVersionJson(name);
|
||||
|
||||
try (FileSystem fs = CompressingUtils.readonly(zipFile.toPath()).setAutoDetectEncoding(true).build()) {
|
||||
try (ZipFileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(zipFile.toPath())) {
|
||||
Path root = MultiMCInstanceConfiguration.getRootPath(fs.getPath("/"));
|
||||
Path patches = root.resolve("patches");
|
||||
|
||||
if (Files.exists(patches)) {
|
||||
if (Files.isDirectory(patches)) {
|
||||
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(patches)) {
|
||||
for (Path patchJson : directoryStream) {
|
||||
if (patchJson.toString().endsWith(".json")) {
|
||||
|
||||
@@ -25,12 +25,12 @@ import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.gson.TolerableValidationException;
|
||||
import org.jackhuang.hmcl.util.gson.Validation;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -129,13 +129,13 @@ public class ServerModpackManifest implements Validation {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param zip the CurseForge modpack file.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
* @param fs the CurseForge modpack file.
|
||||
* @return the manifest.
|
||||
* @throws IOException if the file is not a valid zip file.
|
||||
* @throws JsonParseException if the server-manifest.json is missing or malformed.
|
||||
*/
|
||||
public static Modpack readManifest(Path zip, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = CompressingUtils.readTextZipEntry(zip, "server-manifest.json", encoding);
|
||||
public static Modpack readManifest(FileSystem fs, Charset encoding) throws IOException, JsonParseException {
|
||||
String json = FileUtils.readText(fs.getPath("server-manifest.json"));
|
||||
ServerModpackManifest manifest = JsonUtils.fromNonNullJson(json, ServerModpackManifest.class);
|
||||
return manifest.toModpack(encoding);
|
||||
}
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
|
||||
/**
|
||||
* Commons-lang ArrayUtils
|
||||
*/
|
||||
public final class ArrayUtils {
|
||||
private ArrayUtils() {
|
||||
}
|
||||
|
||||
public static boolean[] addAll(boolean[] array1, boolean... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
boolean[] joinedArray = new boolean[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] addAll(byte[] array1, byte... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
byte[] joinedArray = new byte[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static char[] addAll(char[] array1, char... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
char[] joinedArray = new char[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static double[] addAll(double[] array1, double... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
double[] joinedArray = new double[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static float[] addAll(float[] array1, float... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
float[] joinedArray = new float[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static int[] addAll(int[] array1, int... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
int[] joinedArray = new int[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static long[] addAll(long[] array1, long... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
long[] joinedArray = new long[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
public static short[] addAll(short[] array1, short... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
} else {
|
||||
short[] joinedArray = new short[array1.length + array2.length];
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
return joinedArray;
|
||||
}
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <T> T[] addAll(final T[] array1, final T... array2) {
|
||||
if (array1 == null) {
|
||||
return clone(array2);
|
||||
} else if (array2 == null) {
|
||||
return clone(array1);
|
||||
}
|
||||
final Class<?> type1 = array1.getClass().getComponentType();
|
||||
@SuppressWarnings("unchecked") // OK, because array is of type T
|
||||
final T[] joinedArray = (T[]) Array.newInstance(type1, array1.length + array2.length);
|
||||
System.arraycopy(array1, 0, joinedArray, 0, array1.length);
|
||||
try {
|
||||
System.arraycopy(array2, 0, joinedArray, array1.length, array2.length);
|
||||
} catch (final ArrayStoreException ase) {
|
||||
// Check if problem was due to incompatible types
|
||||
/*
|
||||
* We do this here, rather than before the copy because:
|
||||
* - it would be a wasted check most of the time
|
||||
* - safer, in case check turns out to be too strict
|
||||
*/
|
||||
final Class<?> type2 = array2.getClass().getComponentType();
|
||||
if (!type1.isAssignableFrom(type2)) {
|
||||
throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of "
|
||||
+ type1.getName(), ase);
|
||||
}
|
||||
throw ase; // No, so rethrow original
|
||||
}
|
||||
return joinedArray;
|
||||
}
|
||||
|
||||
public static boolean[] clone(final boolean[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static byte[] clone(final byte[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static char[] clone(final char[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static double[] clone(final double[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static float[] clone(final float[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static int[] clone(final int[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static long[] clone(final long[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static short[] clone(final short[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static <T> T[] clone(final T[] array) {
|
||||
if (array == null) {
|
||||
return null;
|
||||
}
|
||||
return array.clone();
|
||||
}
|
||||
|
||||
public static void reverse(final boolean[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final byte[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final char[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final double[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final float[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final int[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final long[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final short[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final Object[] array) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
reverse(array, 0, array.length);
|
||||
}
|
||||
|
||||
public static void reverse(final boolean[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
boolean tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final byte[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
byte tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final char[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
char tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final double[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
double tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final float[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
float tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final int[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
int tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final long[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
long tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final short[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
short tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public static void reverse(final Object[] array, final int startIndexInclusive, final int endIndexExclusive) {
|
||||
if (array == null) {
|
||||
return;
|
||||
}
|
||||
int i = Math.max(startIndexInclusive, 0);
|
||||
int j = Math.min(array.length, endIndexExclusive) - 1;
|
||||
Object tmp;
|
||||
while (j > i) {
|
||||
tmp = array[j];
|
||||
array[j] = array[i];
|
||||
array[i] = tmp;
|
||||
j--;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,509 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class Range<T> {
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private enum ComparableComparator implements Comparator {
|
||||
INSTANCE;
|
||||
|
||||
/**
|
||||
* Comparable based compare implementation.
|
||||
*
|
||||
* @param obj1 left hand side of comparison
|
||||
* @param obj2 right hand side of comparison
|
||||
* @return negative, 0, positive comparison value
|
||||
*/
|
||||
@Override
|
||||
public int compare(final Object obj1, final Object obj2) {
|
||||
return ((Comparable) obj1).compareTo(obj2);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialization version.
|
||||
*
|
||||
* @see java.io.Serializable
|
||||
*/
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* <p>Obtains a range with the specified minimum and maximum values (both inclusive).</p>
|
||||
*
|
||||
* <p>The range uses the natural ordering of the elements to determine where
|
||||
* values lie in the range.</p>
|
||||
*
|
||||
* <p>The arguments may be passed in the order (min,max) or (max,min).
|
||||
* The getMinimum and getMaximum methods will return the correct values.</p>
|
||||
*
|
||||
* @param <T> the type of the elements in this range
|
||||
* @param fromInclusive the first value that defines the edge of the range, inclusive
|
||||
* @param toInclusive the second value that defines the edge of the range, inclusive
|
||||
* @return the range object, not null
|
||||
* @throws IllegalArgumentException if either element is null
|
||||
* @throws ClassCastException if the elements are not {@code Comparable}
|
||||
*/
|
||||
public static <T extends Comparable<T>> Range<T> between(final T fromInclusive, final T toInclusive) {
|
||||
return between(fromInclusive, toInclusive, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Obtains a range with the specified minimum and maximum values (both inclusive).</p>
|
||||
*
|
||||
* <p>The range uses the specified {@code Comparator} to determine where
|
||||
* values lie in the range.</p>
|
||||
*
|
||||
* <p>The arguments may be passed in the order (min,max) or (max,min).
|
||||
* The getMinimum and getMaximum methods will return the correct values.</p>
|
||||
*
|
||||
* @param <T> the type of the elements in this range
|
||||
* @param fromInclusive the first value that defines the edge of the range, inclusive
|
||||
* @param toInclusive the second value that defines the edge of the range, inclusive
|
||||
* @param comparator the comparator to be used, null for natural ordering
|
||||
* @return the range object, not null
|
||||
* @throws IllegalArgumentException if either element is null
|
||||
* @throws ClassCastException if using natural ordering and the elements are not {@code Comparable}
|
||||
*/
|
||||
public static <T> Range<T> between(final T fromInclusive, final T toInclusive, final Comparator<T> comparator) {
|
||||
return new Range<>(fromInclusive, toInclusive, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Obtains a range using the specified element as both the minimum
|
||||
* and maximum in this range.</p>
|
||||
*
|
||||
* <p>The range uses the natural ordering of the elements to determine where
|
||||
* values lie in the range.</p>
|
||||
*
|
||||
* @param <T> the type of the elements in this range
|
||||
* @param element the value to use for this range, not null
|
||||
* @return the range object, not null
|
||||
* @throws IllegalArgumentException if the element is null
|
||||
* @throws ClassCastException if the element is not {@code Comparable}
|
||||
*/
|
||||
public static <T extends Comparable<T>> Range<T> is(final T element) {
|
||||
return between(element, element, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Obtains a range using the specified element as both the minimum
|
||||
* and maximum in this range.</p>
|
||||
*
|
||||
* <p>The range uses the specified {@code Comparator} to determine where
|
||||
* values lie in the range.</p>
|
||||
*
|
||||
* @param <T> the type of the elements in this range
|
||||
* @param element the value to use for this range, must not be {@code null}
|
||||
* @param comparator the comparator to be used, null for natural ordering
|
||||
* @return the range object, not null
|
||||
* @throws IllegalArgumentException if the element is null
|
||||
* @throws ClassCastException if using natural ordering and the elements are not {@code Comparable}
|
||||
*/
|
||||
public static <T> Range<T> is(final T element, final Comparator<T> comparator) {
|
||||
return between(element, element, comparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* The ordering scheme used in this range.
|
||||
*/
|
||||
private final Comparator<T> comparator;
|
||||
|
||||
/**
|
||||
* Cached output hashCode (class is immutable).
|
||||
*/
|
||||
private transient int hashCode;
|
||||
|
||||
/**
|
||||
* The maximum value in this range (inclusive).
|
||||
*/
|
||||
private final T maximum;
|
||||
|
||||
/**
|
||||
* The minimum value in this range (inclusive).
|
||||
*/
|
||||
private final T minimum;
|
||||
|
||||
/**
|
||||
* Cached output toString (class is immutable).
|
||||
*/
|
||||
private transient String toString;
|
||||
|
||||
/**
|
||||
* Creates an instance.
|
||||
*
|
||||
* @param element1 the first element, not null
|
||||
* @param element2 the second element, not null
|
||||
* @param comp the comparator to be used, null for natural ordering
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Range(final T element1, final T element2, final Comparator<T> comp) {
|
||||
if (element1 == null || element2 == null) {
|
||||
throw new IllegalArgumentException("Elements in a range must not be null: element1=" +
|
||||
element1 + ", element2=" + element2);
|
||||
}
|
||||
if (comp == null) {
|
||||
this.comparator = ComparableComparator.INSTANCE;
|
||||
} else {
|
||||
this.comparator = comp;
|
||||
}
|
||||
if (this.comparator.compare(element1, element2) < 1) {
|
||||
this.minimum = element1;
|
||||
this.maximum = element2;
|
||||
} else {
|
||||
this.minimum = element2;
|
||||
this.maximum = element1;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return comparator.compare(minimum, maximum) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether the specified element occurs within this range.</p>
|
||||
*
|
||||
* @param element the element to check for, null returns false
|
||||
* @return true if the specified element occurs within this range
|
||||
*/
|
||||
public boolean contains(final T element) {
|
||||
if (element == null) {
|
||||
return false;
|
||||
}
|
||||
return comparator.compare(element, minimum) > -1 && comparator.compare(element, maximum) < 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range contains all the elements of the specified range.</p>
|
||||
*
|
||||
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||
*
|
||||
* @param otherRange the range to check, null returns false
|
||||
* @return true if this range contains the specified range
|
||||
* @throws RuntimeException if ranges cannot be compared
|
||||
*/
|
||||
public boolean containsRange(final Range<T> otherRange) {
|
||||
if (otherRange == null) {
|
||||
return false;
|
||||
}
|
||||
return contains(otherRange.minimum)
|
||||
&& contains(otherRange.maximum);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks where the specified element occurs relative to this range.</p>
|
||||
*
|
||||
* <p>The API is reminiscent of the Comparable interface returning {@code -1} if
|
||||
* the element is before the range, {@code 0} if contained within the range and
|
||||
* {@code 1} if the element is after the range. </p>
|
||||
*
|
||||
* @param element the element to check for, not null
|
||||
* @return -1, 0 or +1 depending on the element's location relative to the range
|
||||
*/
|
||||
public int elementCompareTo(final T element) {
|
||||
// Comparable API says throw NPE on null
|
||||
Objects.requireNonNull(element);
|
||||
if (isAfter(element)) {
|
||||
return -1;
|
||||
}
|
||||
if (isBefore(element)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Element tests
|
||||
//--------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* <p>Compares this range to another object to test if they are equal.</p>.
|
||||
*
|
||||
* <p>To be equal, the minimum and maximum values must be equal, which
|
||||
* ignores any differences in the comparator.</p>
|
||||
*
|
||||
* @param obj the reference object with which to compare
|
||||
* @return true if this object is equal
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(final Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || obj.getClass() != getClass()) {
|
||||
return false;
|
||||
}
|
||||
@SuppressWarnings("unchecked") // OK because we checked the class above
|
||||
final Range<T> range = (Range<T>) obj;
|
||||
return minimum.equals(range.minimum) &&
|
||||
maximum.equals(range.maximum);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the comparator being used to determine if objects are within the range.</p>
|
||||
*
|
||||
* <p>Natural ordering uses an internal comparator implementation, thus this
|
||||
* method never returns null. See {@link #isNaturalOrdering()}.</p>
|
||||
*
|
||||
* @return the comparator being used, not null
|
||||
*/
|
||||
public Comparator<T> getComparator() {
|
||||
return comparator;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the maximum value in this range.</p>
|
||||
*
|
||||
* @return the maximum value in this range, not null
|
||||
*/
|
||||
public T getMaximum() {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the minimum value in this range.</p>
|
||||
*
|
||||
* @return the minimum value in this range, not null
|
||||
*/
|
||||
public T getMinimum() {
|
||||
return minimum;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets a suitable hash code for the range.</p>
|
||||
*
|
||||
* @return a hash code value for this object
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = hashCode;
|
||||
if (hashCode == 0) {
|
||||
result = 17;
|
||||
result = 37 * result + getClass().hashCode();
|
||||
result = 37 * result + minimum.hashCode();
|
||||
result = 37 * result + maximum.hashCode();
|
||||
hashCode = result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the intersection of {@code this} and an overlapping Range.
|
||||
*
|
||||
* @param other overlapping Range
|
||||
* @return range representing the intersection of {@code this} and {@code other} ({@code this} if equal)
|
||||
* @throws IllegalArgumentException if {@code other} does not overlap {@code this}
|
||||
* @since 3.0.1
|
||||
*/
|
||||
public Range<T> intersectionWith(final Range<T> other) {
|
||||
if (!this.isOverlappedBy(other)) {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Cannot calculate intersection with non-overlapping range %s", other));
|
||||
}
|
||||
if (this.equals(other)) {
|
||||
return this;
|
||||
}
|
||||
final T min = getComparator().compare(minimum, other.minimum) < 0 ? other.minimum : minimum;
|
||||
final T max = getComparator().compare(maximum, other.maximum) < 0 ? maximum : other.maximum;
|
||||
return between(min, max, getComparator());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range is after the specified element.</p>
|
||||
*
|
||||
* @param element the element to check for, null returns false
|
||||
* @return true if this range is entirely after the specified element
|
||||
*/
|
||||
public boolean isAfter(final T element) {
|
||||
if (element == null) {
|
||||
return false;
|
||||
}
|
||||
return comparator.compare(element, minimum) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range is completely after the specified range.</p>
|
||||
*
|
||||
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||
*
|
||||
* @param otherRange the range to check, null returns false
|
||||
* @return true if this range is completely after the specified range
|
||||
* @throws RuntimeException if ranges cannot be compared
|
||||
*/
|
||||
public boolean isAfterRange(final Range<T> otherRange) {
|
||||
if (otherRange == null) {
|
||||
return false;
|
||||
}
|
||||
return isAfter(otherRange.maximum);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range is before the specified element.</p>
|
||||
*
|
||||
* @param element the element to check for, null returns false
|
||||
* @return true if this range is entirely before the specified element
|
||||
*/
|
||||
public boolean isBefore(final T element) {
|
||||
if (element == null) {
|
||||
return false;
|
||||
}
|
||||
return comparator.compare(element, maximum) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range is completely before the specified range.</p>
|
||||
*
|
||||
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||
*
|
||||
* @param otherRange the range to check, null returns false
|
||||
* @return true if this range is completely before the specified range
|
||||
* @throws RuntimeException if ranges cannot be compared
|
||||
*/
|
||||
public boolean isBeforeRange(final Range<T> otherRange) {
|
||||
if (otherRange == null) {
|
||||
return false;
|
||||
}
|
||||
return isBefore(otherRange.minimum);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range ends with the specified element.</p>
|
||||
*
|
||||
* @param element the element to check for, null returns false
|
||||
* @return true if the specified element occurs within this range
|
||||
*/
|
||||
public boolean isEndedBy(final T element) {
|
||||
if (element == null) {
|
||||
return false;
|
||||
}
|
||||
return comparator.compare(element, maximum) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Whether or not the Range is using the natural ordering of the elements.</p>
|
||||
*
|
||||
* <p>Natural ordering uses an internal comparator implementation, thus this
|
||||
* method is the only way to check if a null comparator was specified.</p>
|
||||
*
|
||||
* @return true if using natural ordering
|
||||
*/
|
||||
public boolean isNaturalOrdering() {
|
||||
return comparator == ComparableComparator.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range is overlapped by the specified range.</p>
|
||||
*
|
||||
* <p>Two ranges overlap if there is at least one element in common.</p>
|
||||
*
|
||||
* <p>This method may fail if the ranges have two different comparators or element types.</p>
|
||||
*
|
||||
* @param otherRange the range to test, null returns false
|
||||
* @return true if the specified range overlaps with this
|
||||
* range; otherwise, {@code false}
|
||||
* @throws RuntimeException if ranges cannot be compared
|
||||
*/
|
||||
public boolean isOverlappedBy(final Range<T> otherRange) {
|
||||
if (otherRange == null) {
|
||||
return false;
|
||||
}
|
||||
return otherRange.contains(minimum)
|
||||
|| otherRange.contains(maximum)
|
||||
|| contains(otherRange.minimum);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Checks whether this range starts with the specified element.</p>
|
||||
*
|
||||
* @param element the element to check for, null returns false
|
||||
* @return true if the specified element occurs within this range
|
||||
*/
|
||||
public boolean isStartedBy(final T element) {
|
||||
if (element == null) {
|
||||
return false;
|
||||
}
|
||||
return comparator.compare(element, minimum) == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Fits the given element into this range by returning the given element or, if out of bounds, the range minimum if
|
||||
* below, or the range maximum if above.
|
||||
* </p>
|
||||
* <pre>
|
||||
* Range<Integer> range = Range.between(16, 64);
|
||||
* range.fit(-9) --> 16
|
||||
* range.fit(0) --> 16
|
||||
* range.fit(15) --> 16
|
||||
* range.fit(16) --> 16
|
||||
* range.fit(17) --> 17
|
||||
* ...
|
||||
* range.fit(63) --> 63
|
||||
* range.fit(64) --> 64
|
||||
* range.fit(99) --> 64
|
||||
* </pre>
|
||||
*
|
||||
* @param element the element to check for, not null
|
||||
* @return the minimum, the element, or the maximum depending on the element's location relative to the range
|
||||
* @since 3.10
|
||||
*/
|
||||
public T fit(final T element) {
|
||||
Objects.requireNonNull(element);
|
||||
if (isAfter(element)) {
|
||||
return minimum;
|
||||
}
|
||||
if (isBefore(element)) {
|
||||
return maximum;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Gets the range as a {@code String}.</p>
|
||||
*
|
||||
* <p>The format of the String is '[<i>min</i>..<i>max</i>]'.</p>
|
||||
*
|
||||
* @return the {@code String} representation of this range
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
if (toString == null) {
|
||||
toString = "[" + minimum + ".." + maximum + "]";
|
||||
}
|
||||
return toString;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Formats the receiver using the given format.</p>
|
||||
*
|
||||
* <p>This uses {@link java.util.Formattable} to perform the formatting. Three variables may
|
||||
* be used to embed the minimum, maximum and comparator.
|
||||
* Use {@code %1$s} for the minimum element, {@code %2$s} for the maximum element
|
||||
* and {@code %3$s} for the comparator.
|
||||
* The default format used by {@code toString()} is {@code [%1$s..%2$s]}.</p>
|
||||
*
|
||||
* @param format the format string, optionally containing {@code %1$s}, {@code %2$s} and {@code %3$s}, not null
|
||||
* @return the formatted string, not null
|
||||
*/
|
||||
public String toString(final String format) {
|
||||
return String.format(format, minimum, maximum, comparator);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -35,8 +36,12 @@ public final class StringUtils {
|
||||
|
||||
public static String getStackTrace(Throwable throwable) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
throwable.printStackTrace(new PrintStream(stream));
|
||||
return stream.toString();
|
||||
try {
|
||||
throwable.printStackTrace(new PrintStream(stream, false, "UTF-8"));
|
||||
return stream.toString("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new InternalError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getStackTrace(StackTraceElement[] elements) {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2021 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.gson;
|
||||
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSerializer;
|
||||
|
||||
public abstract class GsonSerializerHelper<T> implements JsonSerializer<T>, JsonDeserializer<T> {
|
||||
|
||||
protected static void add(JsonObject object, String property, JsonElement value) {
|
||||
if (value == null) return;
|
||||
object.add(property, value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,9 +17,14 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.util.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
||||
import org.jackhuang.hmcl.util.DigestUtils;
|
||||
import org.jackhuang.hmcl.util.Hex;
|
||||
|
||||
public class ChecksumMismatchException extends IOException {
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public class ChecksumMismatchException extends ArtifactMalformedException {
|
||||
|
||||
private final String algorithm;
|
||||
private final String expectedChecksum;
|
||||
@@ -43,4 +48,11 @@ public class ChecksumMismatchException extends IOException {
|
||||
public String getActualChecksum() {
|
||||
return actualChecksum;
|
||||
}
|
||||
|
||||
public static void verifyChecksum(Path file, String algorithm, String expectedChecksum) throws IOException {
|
||||
String checksum = Hex.encodeHex(DigestUtils.digest(algorithm, file));
|
||||
if (!checksum.equalsIgnoreCase(expectedChecksum)) {
|
||||
throw new ChecksumMismatchException(algorithm, expectedChecksum, checksum);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user