Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7108f318a | ||
|
|
a3eaccb45b | ||
|
|
e0c97da9fd | ||
|
|
3843002113 | ||
|
|
1ec9e41459 | ||
|
|
a6231ac593 | ||
|
|
ea91ee91ec | ||
|
|
d4c0c30283 | ||
|
|
9e2c46a6f8 | ||
|
|
ce31bcc6e0 | ||
|
|
d11e22026c | ||
|
|
a96fe298b8 | ||
|
|
8e28c2487f | ||
|
|
fda6f75333 | ||
|
|
e414450f3c | ||
|
|
95e15d7df4 | ||
|
|
7e826a5952 | ||
|
|
43e2644f0e | ||
|
|
a0ef16bef4 | ||
|
|
f5d83095cc | ||
|
|
a94bb407e5 | ||
|
|
6611774aa9 | ||
|
|
e1eb40c129 | ||
|
|
2e532be34c | ||
|
|
b9d0cfc12a | ||
|
|
3d88bc35bb | ||
|
|
8c9c8fd28f | ||
|
|
5e685b7f26 | ||
|
|
c76b562705 | ||
|
|
4c9f8d3dca | ||
|
|
0f26ae5cfc | ||
|
|
035469240d | ||
|
|
7ee81f7425 | ||
|
|
024c90d688 | ||
|
|
1253272626 | ||
|
|
73b938cfd9 | ||
|
|
ee843543bf | ||
|
|
fa8a479aac | ||
|
|
bac14db432 | ||
|
|
f6242ea868 | ||
|
|
16db35655a | ||
|
|
53336aedc9 | ||
|
|
eaddb67932 | ||
|
|
c7b7dd9500 | ||
|
|
9901bf738d | ||
|
|
a3d8c6b3b3 | ||
|
|
948928f740 | ||
|
|
71e98c035e | ||
|
|
af107b15c0 | ||
|
|
8687bb9487 | ||
|
|
a70e53364d | ||
|
|
11a1c37765 | ||
|
|
68a8944810 | ||
|
|
b280b238df | ||
|
|
0f39aad429 | ||
|
|
ff78297ee5 | ||
|
|
f75bbdcecc | ||
|
|
694e31a7fe | ||
|
|
c113670cda | ||
|
|
1b3c399335 | ||
|
|
45cbe20449 | ||
|
|
4d606510f6 | ||
|
|
a6639552cf | ||
|
|
2a088ec555 | ||
|
|
1c46d63367 | ||
|
|
f2b7fae694 | ||
|
|
43986676d8 | ||
|
|
2649e79d47 | ||
|
|
f83d3cd769 | ||
|
|
f124910f94 | ||
|
|
c1712c3e1e | ||
|
|
d817c80629 |
No files matched your search
@@ -8,15 +8,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style main
|
||||
run: ./gradlew checkstyleMain
|
||||
- name: Check style test
|
||||
|
||||
@@ -8,14 +8,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Check style test
|
||||
run: ./gradlew checkTranslations
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'huanghongxun' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
- name: Fetch tags
|
||||
run: git fetch --all --tags
|
||||
- name: Install tools
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
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 "The full changelogs can be found on the website: https://docs.hmcl.net/changelog/dev.html" >> RELEASE_NOTE
|
||||
echo "Notice: changelogs are written in Chinese." >> RELEASE_NOTE
|
||||
- name: Create release
|
||||
if: ${{ env.continue == 'true' }}
|
||||
|
||||
@@ -8,15 +8,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout submodules
|
||||
run: git submodule update --init --recursive
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v2
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '11'
|
||||
java-package: jdk+fx
|
||||
java-package: 'jdk+fx'
|
||||
- name: Build with Gradle
|
||||
run: ./gradlew build
|
||||
env:
|
||||
@@ -26,7 +24,7 @@ jobs:
|
||||
- name: Get short SHA
|
||||
run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: HMCL-${{ env.SHORT_SHA }}
|
||||
path: HMCL/build/libs
|
||||
@@ -12,6 +12,7 @@ hs_err_pid*
|
||||
.mine*
|
||||
/externalgames
|
||||
NVIDIA
|
||||
minecraft-exported-crash-info*
|
||||
|
||||
# gradle build
|
||||
/build/
|
||||
@@ -19,7 +20,6 @@ NVIDIA
|
||||
/HMCLCore/build/
|
||||
/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/build/
|
||||
/minecraft/libraries/log4j-patch/build/
|
||||
|
||||
# idea
|
||||
.idea
|
||||
@@ -27,14 +27,12 @@ NVIDIA
|
||||
/HMCL/out/
|
||||
/HMCLCore/out/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/out/
|
||||
/minecraft/libraries/log4j-patch/out/
|
||||
|
||||
# eclipse
|
||||
/bin/
|
||||
/HMCL/bin/
|
||||
/HMCLCore/bin/
|
||||
/minecraft/libraries/HMCLTransformerDiscoveryService/bin/
|
||||
/minecraft/libraries/log4j-patch/bin/
|
||||
.classpath
|
||||
.project
|
||||
.settings
|
||||
|
||||
@@ -28,7 +28,7 @@ buildscript {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id("com.github.johnrengelman.shadow") version "7.0.0"
|
||||
id("com.github.johnrengelman.shadow") version "7.1.2"
|
||||
}
|
||||
|
||||
val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
@@ -37,7 +37,8 @@ val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number ->
|
||||
(number - offset).toString()
|
||||
} else {
|
||||
val shortCommit = System.getenv("GITHUB_SHA")?.toLowerCase()?.substring(0, 7)
|
||||
if (!shortCommit.isNullOrEmpty()) "dev-$shortCommit" else "SNAPSHOT"
|
||||
val prefix = if (System.getenv("GITHUB_REPOSITORY_OWNER") == "huanghongxun") "dev" else "unofficial"
|
||||
if (!shortCommit.isNullOrEmpty()) "$prefix-$shortCommit" else "SNAPSHOT"
|
||||
}
|
||||
}
|
||||
val versionRoot = System.getenv("VERSION_ROOT") ?: "3.5"
|
||||
|
||||
@@ -16,8 +16,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class SkinCanvas extends Group {
|
||||
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/steve.png"));
|
||||
public static final Image ALEX = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/alex.png"));
|
||||
public static final Image STEVE = new Image(SkinCanvas.class.getResourceAsStream("/assets/img/skin/steve.png"));
|
||||
|
||||
public static final SkinCube ALEX_LARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 32F / 64F, 48F / 64F, 0F, true);
|
||||
public static final SkinCube ALEX_RARM = new SkinCube(3, 12, 4, 14F / 64F, 16F / 64F, 40F / 64F, 16F / 64F, 0F, true);
|
||||
|
||||
@@ -19,26 +19,35 @@ package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.DataFormat;
|
||||
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;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.CrashReporter;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.net.*;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -62,7 +71,51 @@ public final class Launcher extends Application {
|
||||
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()));
|
||||
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS) {
|
||||
String owner = Files.getOwner(ConfigHolder.configLocation()).getName();
|
||||
String userName = System.getProperty("user.name");
|
||||
if (!Files.isWritable(ConfigHolder.configLocation())
|
||||
&& !userName.equals("root")
|
||||
&& !userName.equals(owner)) {
|
||||
|
||||
ArrayList<String> files = new ArrayList<>();
|
||||
{
|
||||
files.add(ConfigHolder.configLocation().toString());
|
||||
if (Files.exists(Metadata.HMCL_DIRECTORY))
|
||||
files.add(Metadata.HMCL_DIRECTORY.toString());
|
||||
|
||||
Path mcDir = Paths.get(".minecraft").toAbsolutePath().normalize();
|
||||
if (Files.exists(mcDir))
|
||||
files.add(mcDir.toString());
|
||||
}
|
||||
|
||||
String command = new CommandBuilder().add("sudo", "chown", "-R", userName).addAll(files).toString();
|
||||
ButtonType copyAndExit = new ButtonType(i18n("button.copy_and_exit"));
|
||||
|
||||
ButtonType res = new Alert(Alert.AlertType.ERROR, i18n("fatal.config_loading_failure.unix", owner, command), copyAndExit, ButtonType.CLOSE)
|
||||
.showAndWait()
|
||||
.orElse(null);
|
||||
if (res == copyAndExit) {
|
||||
Clipboard.getSystemClipboard()
|
||||
.setContent(Collections.singletonMap(DataFormat.PLAIN_TEXT, command));
|
||||
}
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
LOG.log(Level.WARNING, "Failed to get file owner", ioe);
|
||||
}
|
||||
Main.showErrorAndExit(i18n("fatal.config_loading_failure", Paths.get("").toAbsolutePath().normalize().toString()));
|
||||
}
|
||||
|
||||
if (ConfigHolder.isOwnerChanged()) {
|
||||
ButtonType res = new Alert(Alert.AlertType.WARNING, i18n("fatal.config_change_owner_root"), ButtonType.YES, ButtonType.NO)
|
||||
.showAndWait()
|
||||
.orElse(null);
|
||||
if (res == ButtonType.NO)
|
||||
return;
|
||||
}
|
||||
|
||||
if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {
|
||||
@@ -76,8 +129,6 @@ public final class Launcher extends Application {
|
||||
Platform.setImplicitExit(false);
|
||||
Controllers.initialize(primaryStage);
|
||||
|
||||
initIcon();
|
||||
|
||||
UpdateChecker.init();
|
||||
|
||||
primaryStage.show();
|
||||
@@ -93,12 +144,6 @@ public final class Launcher extends Application {
|
||||
Controllers.onApplicationStop();
|
||||
}
|
||||
|
||||
private void initIcon() {
|
||||
Toolkit toolkit = Toolkit.getDefaultToolkit();
|
||||
Image image = toolkit.getImage(Launcher.class.getResource("/assets/img/icon.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (UpdateHandler.processArguments(args)) {
|
||||
return;
|
||||
@@ -117,6 +162,7 @@ public final class Launcher extends Application {
|
||||
LOG.info("Java Home: " + System.getProperty("java.home"));
|
||||
LOG.info("Current Directory: " + Paths.get("").toAbsolutePath());
|
||||
LOG.info("HMCL Directory: " + Metadata.HMCL_DIRECTORY);
|
||||
LOG.info("HMCL Jar Path: " + JarUtils.thisJar().map(it -> it.toAbsolutePath().toString()).orElse("Not Found"));
|
||||
LOG.info("Memory: " + Runtime.getRuntime().maxMemory() / 1024 / 1024 + "MB");
|
||||
ManagementFactory.getMemoryPoolMXBeans().stream().filter(bean -> bean.getName().equals("Metaspace")).findAny()
|
||||
.ifPresent(bean -> LOG.info("Metaspace: " + bean.getUsage().getUsed() / 1024 / 1024 + "MB"));
|
||||
|
||||
@@ -17,14 +17,19 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Alert;
|
||||
import org.jackhuang.hmcl.ui.AwtUtils;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.SelfDependencyPatcher;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
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;
|
||||
import java.io.InputStream;
|
||||
@@ -56,29 +61,25 @@ public final class Main {
|
||||
// Fix title bar not displaying in GTK systems
|
||||
System.setProperty("jdk.gtk.version", "2");
|
||||
|
||||
// Use System look and feel
|
||||
initLookAndFeel();
|
||||
|
||||
checkDirectoryPath();
|
||||
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9)
|
||||
// This environment check will take ~300ms
|
||||
thread(Main::fixLetsEncrypt, "CA Certificate Check", true);
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
initIcon();
|
||||
|
||||
Logging.start(Metadata.HMCL_DIRECTORY.resolve("logs"));
|
||||
|
||||
checkJavaFX();
|
||||
|
||||
|
||||
Launcher.main(args);
|
||||
}
|
||||
|
||||
private static void initLookAndFeel() {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
private static void initIcon() {
|
||||
java.awt.Image image = java.awt.Toolkit.getDefaultToolkit().getImage(Main.class.getResource("/assets/img/icon@8x.png"));
|
||||
AwtUtils.setAppleIcon(image);
|
||||
}
|
||||
|
||||
private static void checkDirectoryPath() {
|
||||
@@ -116,7 +117,16 @@ public final class Main {
|
||||
static void showErrorAndExit(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("A fatal error has occurred, forcibly exiting.");
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.ERROR, message).showAndWait();
|
||||
System.exit(1);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showErrorDialog(message);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
@@ -126,7 +136,16 @@ public final class Main {
|
||||
static void showWarningAndContinue(String message) {
|
||||
System.err.println(message);
|
||||
System.err.println("Potential issues have been detected.");
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
try {
|
||||
if (Platform.isFxApplicationThread()) {
|
||||
new Alert(Alert.AlertType.WARNING, message).showAndWait();
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
SwingUtils.showWarningDialog(message);
|
||||
}
|
||||
|
||||
static void fixLetsEncrypt() {
|
||||
@@ -156,7 +175,8 @@ public final class Main {
|
||||
tls.init(null, instance.getTrustManagers(), null);
|
||||
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
|
||||
LOG.info("Added Lets Encrypt root certificates as additional trust");
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException | KeyManagementException e) {
|
||||
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException |
|
||||
KeyManagementException e) {
|
||||
LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,9 @@
|
||||
package org.jackhuang.hmcl;
|
||||
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Stores metadata about this application.
|
||||
@@ -32,7 +30,7 @@ public final class Metadata {
|
||||
|
||||
public static final String NAME = "HMCL";
|
||||
public static final String FULL_NAME = "Hello Minecraft! Launcher";
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.thisJar().flatMap(JarUtils::getImplementationVersion).orElse("@develop@"));
|
||||
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getManifestAttribute("Implementation-Version", "@develop@"));
|
||||
|
||||
public static final String TITLE = NAME + " " + VERSION;
|
||||
public static final String FULL_TITLE = FULL_NAME + " v" + VERSION;
|
||||
@@ -40,27 +38,14 @@ public final class Metadata {
|
||||
public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link");
|
||||
public static final String CONTACT_URL = "https://github.com/huanghongxun/HMCL/issues";
|
||||
public static final String HELP_URL = "https://hmcl.huangyuhui.net/help";
|
||||
public static final String CHANGELOG_URL = "https://hmcl.huangyuhui.net/changelog/";
|
||||
public static final String PUBLISH_URL = "http://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/";
|
||||
public static final String PUBLISH_URL = "https://www.mcbbs.net/thread-142335-1-1.html";
|
||||
public static final String EULA_URL = "https://hmcl.huangyuhui.net/eula";
|
||||
|
||||
public static final String BUILD_CHANNEL = JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Build-Channel")).orElse("nightly");
|
||||
public static final String BUILD_CHANNEL = JarUtils.getManifestAttribute("Build-Channel", "nightly");
|
||||
|
||||
public static final Path MINECRAFT_DIRECTORY = OperatingSystem.getWorkingDirectory("minecraft");
|
||||
public static final Path HMCL_DIRECTORY = getHMCLDirectory();
|
||||
|
||||
private static Path getHMCLDirectory() {
|
||||
String home = System.getProperty("user.home", ".");
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
// to fulfill XDG standard.
|
||||
String xdgCache = System.getenv("XDG_CACHE_HOME");
|
||||
if (StringUtils.isNotBlank(xdgCache)) {
|
||||
return Paths.get(xdgCache, "hmcl");
|
||||
}
|
||||
return Paths.get(home, ".cache", "hmcl");
|
||||
}
|
||||
return OperatingSystem.getWorkingDirectory("hmcl");
|
||||
}
|
||||
public static final Path HMCL_DIRECTORY = OperatingSystem.getWorkingDirectory("hmcl");
|
||||
|
||||
public static boolean isStable() {
|
||||
return "stable".equals(BUILD_CHANNEL);
|
||||
|
||||
@@ -337,7 +337,7 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
.setProfileName(Metadata.TITLE)
|
||||
.setGameArguments(StringUtils.tokenize(vs.getMinecraftArgs()))
|
||||
.setJavaArguments(StringUtils.tokenize(vs.getJavaArgs()))
|
||||
.setMaxMemory((int)(getAllocatedMemory(
|
||||
.setMaxMemory(vs.isNoJVMArgs() && vs.isAutoMemory() ? null : (int)(getAllocatedMemory(
|
||||
vs.getMaxMemory() * 1024L * 1024L,
|
||||
OperatingSystem.getPhysicalMemoryStatus().orElse(OperatingSystem.PhysicalMemoryStatus.INVALID).getAvailable(),
|
||||
vs.isAutoMemory()
|
||||
@@ -380,6 +380,9 @@ public class HMCLGameRepository extends DefaultGameRepository {
|
||||
}
|
||||
}
|
||||
|
||||
if (vs.isAutoMemory() && builder.getJavaArguments().stream().anyMatch(it -> it.startsWith("-Xmx")))
|
||||
builder.setMaxMemory(null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import com.jfoenix.controls.JFXButton;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
import org.jackhuang.hmcl.Launcher;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.*;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDownloadException;
|
||||
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
||||
@@ -47,13 +46,9 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -72,7 +67,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
public final class LauncherHelper {
|
||||
|
||||
private final Profile profile;
|
||||
private final Account account;
|
||||
private Account account;
|
||||
private final String selectedVersion;
|
||||
private File scriptFile;
|
||||
private final VersionSetting setting;
|
||||
@@ -91,6 +86,14 @@ public final class LauncherHelper {
|
||||
|
||||
private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void setAccount(Account account) {
|
||||
this.account = account;
|
||||
}
|
||||
|
||||
public void setTestMode() {
|
||||
launcherVisibility = LauncherVisibility.KEEP;
|
||||
showLogs = true;
|
||||
@@ -126,8 +129,6 @@ public final class LauncherHelper {
|
||||
|
||||
AtomicReference<JavaVersion> javaVersionRef = new AtomicReference<>();
|
||||
|
||||
getLog4jPatch(version.get()).ifPresent(javaAgents::add);
|
||||
|
||||
TaskExecutor executor = checkGameState(profile, setting, version.get())
|
||||
.thenComposeAsync(javaVersion -> {
|
||||
javaVersionRef.set(Objects.requireNonNull(javaVersion));
|
||||
@@ -627,11 +628,11 @@ public final class LauncherHelper {
|
||||
|
||||
return Task.completed(DialogController.logIn(account));
|
||||
} catch (AuthenticationException e) {
|
||||
LOG.log(Level.WARNING, "Authentication failed, try playing offline", e);
|
||||
LOG.log(Level.WARNING, "Authentication failed, try skipping refresh", e);
|
||||
|
||||
CompletableFuture<Task<AuthInfo>> future = new CompletableFuture<>();
|
||||
runInFX(() -> {
|
||||
JFXButton loginOfflineButton = new JFXButton(i18n("account.login.offline"));
|
||||
JFXButton loginOfflineButton = new JFXButton(i18n("account.login.skip"));
|
||||
loginOfflineButton.setOnAction(event -> {
|
||||
try {
|
||||
future.complete(Task.completed(account.playOffline()));
|
||||
@@ -639,7 +640,7 @@ public final class LauncherHelper {
|
||||
future.completeExceptionally(e2);
|
||||
}
|
||||
});
|
||||
JFXButton retryButton = new JFXButton(i18n("button.retry"));
|
||||
JFXButton retryButton = new JFXButton(i18n("account.login.retry"));
|
||||
retryButton.setOnAction(event -> {
|
||||
future.complete(logIn(account));
|
||||
});
|
||||
@@ -655,47 +656,6 @@ public final class LauncherHelper {
|
||||
});
|
||||
}
|
||||
|
||||
private static Optional<String> getLog4jPatch(Version version) {
|
||||
Optional<String> log4jVersion = version.getLibraries().stream()
|
||||
.filter(it -> it.is("org.apache.logging.log4j", "log4j-core")
|
||||
&& (VersionNumber.VERSION_COMPARATOR.compare(it.getVersion(), "2.17") < 0 || "2.0-beta9".equals(it.getVersion())))
|
||||
.map(Library::getVersion)
|
||||
.findFirst();
|
||||
|
||||
if (log4jVersion.isPresent()) {
|
||||
final String agentFileName = "log4j-patch-agent-1.0.jar";
|
||||
|
||||
Path agentFile = Metadata.HMCL_DIRECTORY.resolve(agentFileName).toAbsolutePath();
|
||||
String agentFilePath = agentFile.toString();
|
||||
if (agentFilePath.indexOf('=') >= 0) {
|
||||
LOG.warning("Invalid character '=' in the HMCL directory path, unable to attach log4j-patch");
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
if (Files.notExists(agentFile)) {
|
||||
try (InputStream input = DefaultLauncher.class.getResourceAsStream("/assets/game/" + agentFileName)) {
|
||||
LOG.info("Extract log4j patch to " + agentFilePath);
|
||||
Files.createDirectories(agentFile.getParent());
|
||||
Files.copy(input, agentFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to extract log4j patch");
|
||||
try {
|
||||
Files.deleteIfExists(agentFile);
|
||||
} catch (IOException ex) {
|
||||
LOG.log(Level.WARNING, "Failed to delete incomplete log4j patch", ex);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
boolean isBeta = log4jVersion.get().startsWith("2.0-beta");
|
||||
return Optional.of(agentFilePath + "=" + isBeta);
|
||||
} else {
|
||||
LOG.info("No log4j with security vulnerabilities found");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkExit() {
|
||||
switch (launcherVisibility) {
|
||||
case HIDE_AND_REOPEN:
|
||||
@@ -725,7 +685,7 @@ public final class LauncherHelper {
|
||||
* Guarantee that one [JavaProcess], one [HMCLProcessListener].
|
||||
* Because every time we launched a game, we generates a new [HMCLProcessListener]
|
||||
*/
|
||||
class HMCLProcessListener implements ProcessListener {
|
||||
private final class HMCLProcessListener implements ProcessListener {
|
||||
|
||||
private final HMCLGameRepository repository;
|
||||
private final Version version;
|
||||
@@ -734,9 +694,11 @@ public final class LauncherHelper {
|
||||
private boolean lwjgl;
|
||||
private LogWindow logWindow;
|
||||
private final boolean detectWindow;
|
||||
private final LinkedList<Pair<String, Log4jLevel>> logs;
|
||||
private final ArrayDeque<String> logs;
|
||||
private final ArrayDeque</*Log4jLevel*/Object> levels;
|
||||
private final CountDownLatch logWindowLatch = new CountDownLatch(1);
|
||||
private final CountDownLatch launchingLatch;
|
||||
private final String forbiddenAccessToken;
|
||||
|
||||
public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) {
|
||||
this.repository = repository;
|
||||
@@ -744,8 +706,11 @@ public final class LauncherHelper {
|
||||
this.launchOptions = launchOptions;
|
||||
this.launchingLatch = launchingLatch;
|
||||
this.detectWindow = detectWindow;
|
||||
this.forbiddenAccessToken = authInfo != null ? authInfo.getAccessToken() : null;
|
||||
|
||||
logs = new LinkedList<>();
|
||||
final int numLogs = config().getLogLines() + 1;
|
||||
this.logs = new ArrayDeque<>(numLogs);
|
||||
this.levels = new ArrayDeque<>(numLogs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -796,6 +761,7 @@ public final class LauncherHelper {
|
||||
Controllers.getStage().close();
|
||||
Controllers.shutdown();
|
||||
Schedulers.shutdown();
|
||||
System.gc();
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -803,17 +769,28 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void onLog(String log, Log4jLevel level) {
|
||||
String filteredLog = Logging.filterForbiddenToken(log);
|
||||
public void onLog(String log, boolean isErrorStream) {
|
||||
String filteredLog = forbiddenAccessToken == null ? log : log.replace(forbiddenAccessToken, "<access token>");
|
||||
|
||||
if (level.lessOrEqual(Log4jLevel.ERROR))
|
||||
if (isErrorStream)
|
||||
System.err.println(filteredLog);
|
||||
else
|
||||
System.out.println(filteredLog);
|
||||
|
||||
logs.add(pair(filteredLog, level));
|
||||
if (logs.size() > config().getLogLines())
|
||||
logs.removeFirst();
|
||||
Log4jLevel level;
|
||||
if (isErrorStream)
|
||||
level = Log4jLevel.ERROR;
|
||||
else
|
||||
level = showLogs ? Optional.ofNullable(Log4jLevel.guessLevel(filteredLog)).orElse(Log4jLevel.INFO) : null;
|
||||
|
||||
synchronized (this) {
|
||||
logs.add(filteredLog);
|
||||
levels.add(level != null ? level : Optional.empty()); // Use 'Optional.empty()' as hole
|
||||
if (logs.size() > config().getLogLines()) {
|
||||
logs.removeFirst();
|
||||
levels.removeFirst();
|
||||
}
|
||||
}
|
||||
|
||||
if (showLogs) {
|
||||
try {
|
||||
@@ -826,7 +803,7 @@ public final class LauncherHelper {
|
||||
Platform.runLater(() -> logWindow.logLine(filteredLog, level));
|
||||
}
|
||||
|
||||
if (!lwjgl && (filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal") || !detectWindow)) {
|
||||
if (!lwjgl && (!detectWindow || filteredLog.toLowerCase().contains("lwjgl version") || filteredLog.toLowerCase().contains("lwjgl openal"))) {
|
||||
lwjgl = true;
|
||||
finishLaunch();
|
||||
}
|
||||
@@ -843,8 +820,11 @@ public final class LauncherHelper {
|
||||
if (!lwjgl) finishLaunch();
|
||||
|
||||
if (exitType != ExitType.NORMAL) {
|
||||
ArrayList<Pair<String, Log4jLevel>> pairs = new ArrayList<>(logs.size());
|
||||
Lang.forEachZipped(logs, levels,
|
||||
(log, l) -> pairs.add(pair(log, l instanceof Log4jLevel ? ((Log4jLevel) l) : Optional.ofNullable(Log4jLevel.guessLevel(log)).orElse(Log4jLevel.INFO))));
|
||||
repository.markVersionLaunchedAbnormally(version.getId());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show());
|
||||
Platform.runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, pairs).show());
|
||||
}
|
||||
|
||||
checkExit();
|
||||
|
||||
@@ -30,7 +30,6 @@ import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -104,7 +103,7 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
|
||||
String html;
|
||||
try {
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"), StandardCharsets.UTF_8)
|
||||
html = IOUtils.readFullyAsString(OAuthServer.class.getResourceAsStream("/assets/microsoft_auth.html"))
|
||||
.replace("%close-page%", i18n("account.methods.microsoft.close_page"));
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Failed to load html");
|
||||
@@ -160,13 +159,13 @@ public final class OAuthServer extends NanoHTTPD implements OAuth.Session {
|
||||
@Override
|
||||
public String getClientId() {
|
||||
return System.getProperty("hmcl.microsoft.auth.id",
|
||||
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Id")).orElse(""));
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Id", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientSecret() {
|
||||
return System.getProperty("hmcl.microsoft.auth.secret",
|
||||
JarUtils.thisJar().flatMap(JarUtils::getManifest).map(manifest -> manifest.getMainAttributes().getValue("Microsoft-Auth-Secret")).orElse(""));
|
||||
JarUtils.getManifestAttribute("Microsoft-Auth-Secret", ""));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -17,9 +17,13 @@
|
||||
*/
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.ServerResponseMalformedException;
|
||||
@@ -31,18 +35,15 @@ import org.jackhuang.hmcl.util.ResourceNotFoundError;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -64,15 +65,15 @@ public final class TexturesLoader {
|
||||
|
||||
// ==== Texture Loading ====
|
||||
public static class LoadedTexture {
|
||||
private final BufferedImage image;
|
||||
private final Image image;
|
||||
private final Map<String, String> metadata;
|
||||
|
||||
public LoadedTexture(BufferedImage image, Map<String, String> metadata) {
|
||||
public LoadedTexture(Image image, Map<String, String> metadata) {
|
||||
this.image = requireNonNull(image);
|
||||
this.metadata = requireNonNull(metadata);
|
||||
}
|
||||
|
||||
public BufferedImage getImage() {
|
||||
public Image getImage() {
|
||||
return image;
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ public final class TexturesLoader {
|
||||
return TEXTURES_DIR.resolve(prefix).resolve(hash);
|
||||
}
|
||||
|
||||
public static LoadedTexture loadTexture(Texture texture) throws IOException {
|
||||
public static LoadedTexture loadTexture(Texture texture) throws Throwable {
|
||||
if (StringUtils.isBlank(texture.getUrl())) {
|
||||
throw new IOException("Texture url is empty");
|
||||
}
|
||||
@@ -117,12 +118,13 @@ public final class TexturesLoader {
|
||||
}
|
||||
}
|
||||
|
||||
BufferedImage img;
|
||||
Image img;
|
||||
try (InputStream in = Files.newInputStream(file)) {
|
||||
img = ImageIO.read(in);
|
||||
img = new Image(in);
|
||||
}
|
||||
if (img == null)
|
||||
throw new IOException("Texture is malformed");
|
||||
|
||||
if (img.isError())
|
||||
throw img.getException();
|
||||
|
||||
Map<String, String> metadata = texture.getMetadata();
|
||||
if (metadata == null) {
|
||||
@@ -136,16 +138,21 @@ public final class TexturesLoader {
|
||||
private final static Map<TextureModel, LoadedTexture> DEFAULT_SKINS = new EnumMap<>(TextureModel.class);
|
||||
|
||||
static {
|
||||
loadDefaultSkin("/assets/img/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/alex.png", TextureModel.ALEX);
|
||||
loadDefaultSkin("/assets/img/skin/steve.png", TextureModel.STEVE);
|
||||
loadDefaultSkin("/assets/img/skin/alex.png", TextureModel.ALEX);
|
||||
}
|
||||
|
||||
private static void loadDefaultSkin(String path, TextureModel model) {
|
||||
try (InputStream in = ResourceNotFoundError.getResourceAsStream(path)) {
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(ImageIO.read(in), singletonMap("model", model.modelName)));
|
||||
Image skin;
|
||||
try {
|
||||
skin = new Image(path);
|
||||
if (skin.isError())
|
||||
throw skin.getException();
|
||||
} catch (Throwable e) {
|
||||
throw new ResourceNotFoundError("Cannoot load default skin from " + path, e);
|
||||
throw new ResourceNotFoundError("Cannot load default skin from " + path, e);
|
||||
}
|
||||
|
||||
DEFAULT_SKINS.put(model, new LoadedTexture(skin, singletonMap("model", model.modelName)));
|
||||
}
|
||||
|
||||
public static LoadedTexture getDefaultSkin(TextureModel model) {
|
||||
@@ -172,7 +179,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@@ -195,7 +202,7 @@ public final class TexturesLoader {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
return loadTexture(texture);
|
||||
} catch (IOException e) {
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Failed to load texture " + texture.getUrl() + ", using fallback texture", e);
|
||||
return uuidFallback;
|
||||
}
|
||||
@@ -209,38 +216,109 @@ public final class TexturesLoader {
|
||||
// ====
|
||||
|
||||
// ==== Avatar ====
|
||||
public static BufferedImage toAvatar(BufferedImage skin, int size) {
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = avatar.createGraphics();
|
||||
public static void drawAvatar(Canvas canvas, Image skin) {
|
||||
canvas.getGraphicsContext2D().clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
|
||||
int scale = skin.getWidth() / 64;
|
||||
int size = (int) canvas.getWidth();
|
||||
int scale = (int) skin.getWidth() / 64;
|
||||
int faceOffset = (int) Math.round(size / 18.0);
|
||||
|
||||
GraphicsContext g = canvas.getGraphicsContext2D();
|
||||
try {
|
||||
g.setImageSmoothing(false);
|
||||
drawAvatarFX(g, skin, size, scale, faceOffset);
|
||||
} catch (NoSuchMethodError ignored) {
|
||||
// Earlier JavaFX did not support GraphicsContext::setImageSmoothing, fallback to Java 2D
|
||||
drawAvatarJ2D(g, skin, size, scale, faceOffset);
|
||||
}
|
||||
}
|
||||
|
||||
private static void drawAvatarFX(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
g.drawImage(skin,
|
||||
8 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
faceOffset, faceOffset, size - 2 * faceOffset, size - 2 * faceOffset);
|
||||
g.drawImage(skin,
|
||||
40 * scale, 8 * scale, 8 * scale, 8 * scale,
|
||||
0, 0, size, size);
|
||||
}
|
||||
|
||||
private static void drawAvatarJ2D(GraphicsContext g, Image skin, int size, int scale, int faceOffset) {
|
||||
BufferedImage bi = FXUtils.fromFXImage(skin);
|
||||
|
||||
BufferedImage avatar = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = avatar.createGraphics();
|
||||
|
||||
g2d.drawImage(bi,
|
||||
faceOffset, faceOffset, size - faceOffset, size - faceOffset,
|
||||
8 * scale, 8 * scale, 16 * scale, 16 * scale,
|
||||
null);
|
||||
g.drawImage(skin,
|
||||
g2d.drawImage(bi,
|
||||
0, 0, size, size,
|
||||
40 * scale, 8 * scale, 48 * scale, 16 * scale, null);
|
||||
|
||||
g.dispose();
|
||||
return avatar;
|
||||
g2d.dispose();
|
||||
|
||||
PixelWriter pw = g.getPixelWriter();
|
||||
|
||||
for (int x = 0; x < size; x++) {
|
||||
for (int y = 0; y < size; y++) {
|
||||
pw.setArgb(x, y, avatar.getRGB(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(YggdrasilService service, UUID uuid, int size) {
|
||||
return BindingMapping.of(skinBinding(service, uuid))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
private static final class SkinBindingChangeListener implements ChangeListener<LoadedTexture> {
|
||||
static final WeakHashMap<Canvas, SkinBindingChangeListener> hole = new WeakHashMap<>();
|
||||
|
||||
final WeakReference<Canvas> canvasRef;
|
||||
final ObjectBinding<LoadedTexture> binding;
|
||||
|
||||
SkinBindingChangeListener(Canvas canvas, ObjectBinding<LoadedTexture> binding) {
|
||||
this.canvasRef = new WeakReference<>(canvas);
|
||||
this.binding = binding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends LoadedTexture> observable,
|
||||
LoadedTexture oldValue, LoadedTexture loadedTexture) {
|
||||
Canvas canvas = canvasRef.get();
|
||||
if (canvas != null)
|
||||
drawAvatar(canvas, loadedTexture.image);
|
||||
}
|
||||
}
|
||||
|
||||
public static ObjectBinding<Image> fxAvatarBinding(Account account, int size) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount) {
|
||||
return BindingMapping.of(skinBinding(account))
|
||||
.map(it -> toAvatar(it.image, size))
|
||||
.map(FXUtils::toFXImage);
|
||||
} else {
|
||||
return Bindings.createObjectBinding(
|
||||
() -> FXUtils.toFXImage(toAvatar(getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image, size)));
|
||||
public static void fxAvatarBinding(Canvas canvas, ObjectBinding<LoadedTexture> skinBinding) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
|
||||
SkinBindingChangeListener listener = new SkinBindingChangeListener(canvas, skinBinding);
|
||||
listener.changed(skinBinding, null, skinBinding.get());
|
||||
skinBinding.addListener(listener);
|
||||
|
||||
SkinBindingChangeListener.hole.put(canvas, listener);
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, YggdrasilService service, UUID uuid) {
|
||||
fxAvatarBinding(canvas, skinBinding(service, uuid));
|
||||
}
|
||||
|
||||
public static void bindAvatar(Canvas canvas, Account account) {
|
||||
if (account instanceof YggdrasilAccount || account instanceof MicrosoftAccount)
|
||||
fxAvatarBinding(canvas, skinBinding(account));
|
||||
else {
|
||||
unbindAvatar(canvas);
|
||||
drawAvatar(canvas, getDefaultSkin(TextureModel.detectUUID(account.getUUID())).image);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unbindAvatar(Canvas canvas) {
|
||||
synchronized (SkinBindingChangeListener.hole) {
|
||||
SkinBindingChangeListener oldListener = SkinBindingChangeListener.hole.remove(canvas);
|
||||
if (oldListener != null)
|
||||
oldListener.binding.removeListener(oldListener);
|
||||
}
|
||||
}
|
||||
// ====
|
||||
|
||||
@@ -141,7 +141,7 @@ public final class Config implements Cloneable, Observable {
|
||||
private StringProperty versionListSource = new SimpleStringProperty("balanced");
|
||||
|
||||
@SerializedName("configurations")
|
||||
private ObservableMap<String, Profile> configurations = FXCollections.observableMap(new TreeMap<>());
|
||||
private SimpleMapProperty<String, Profile> configurations = new SimpleMapProperty<>(FXCollections.observableMap(new TreeMap<>()));
|
||||
|
||||
@SerializedName("accounts")
|
||||
private ObservableList<Map<Object, Object>> accountStorages = FXCollections.observableArrayList();
|
||||
@@ -476,7 +476,7 @@ public final class Config implements Cloneable, Observable {
|
||||
return versionListSource;
|
||||
}
|
||||
|
||||
public ObservableMap<String, Profile> getConfigurations() {
|
||||
public MapProperty<String, Profile> getConfigurations() {
|
||||
return configurations;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.util.InvocationDispatcher;
|
||||
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.OperatingSystem;
|
||||
|
||||
@@ -45,6 +46,7 @@ public final class ConfigHolder {
|
||||
private static Config configInstance;
|
||||
private static GlobalConfig globalConfigInstance;
|
||||
private static boolean newlyCreated;
|
||||
private static boolean ownerChanged = false;
|
||||
|
||||
public static Config config() {
|
||||
if (configInstance == null) {
|
||||
@@ -60,10 +62,18 @@ public final class ConfigHolder {
|
||||
return globalConfigInstance;
|
||||
}
|
||||
|
||||
public static Path configLocation() {
|
||||
return configLocation;
|
||||
}
|
||||
|
||||
public static boolean isNewlyCreated() {
|
||||
return newlyCreated;
|
||||
}
|
||||
|
||||
public static boolean isOwnerChanged() {
|
||||
return ownerChanged;
|
||||
}
|
||||
|
||||
public synchronized static void init() throws IOException {
|
||||
if (configInstance != null) {
|
||||
throw new IllegalStateException("Configuration is already loaded");
|
||||
@@ -109,7 +119,7 @@ public final class ConfigHolder {
|
||||
}
|
||||
|
||||
private static Path locateConfig() {
|
||||
Path exePath = Paths.get("");
|
||||
Path exePath = Paths.get("").toAbsolutePath();
|
||||
try {
|
||||
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
|
||||
.toURI()).toAbsolutePath();
|
||||
@@ -143,6 +153,15 @@ public final class ConfigHolder {
|
||||
|
||||
private static Config loadConfig() throws IOException {
|
||||
if (Files.exists(configLocation)) {
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS != OperatingSystem.WINDOWS
|
||||
&& "root".equals(System.getProperty("user.name"))
|
||||
&& !"root".equals(Files.getOwner(configLocation).getName())) {
|
||||
ownerChanged = true;
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
LOG.log(Level.WARNING, "Failed to get owner");
|
||||
}
|
||||
try {
|
||||
String content = FileUtils.readText(configLocation);
|
||||
Config deserialized = Config.fromJson(content);
|
||||
@@ -189,6 +208,28 @@ public final class ConfigHolder {
|
||||
// Global Config
|
||||
|
||||
private static GlobalConfig loadGlobalConfig() throws IOException {
|
||||
// Migrate from old directory
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Files.notExists(GLOBAL_CONFIG_PATH)) {
|
||||
Path oldHome;
|
||||
String xdgCache = System.getenv("XDG_CACHE_HOME");
|
||||
if (StringUtils.isNotBlank(xdgCache)) {
|
||||
oldHome = Paths.get(xdgCache, "hmcl");
|
||||
} else {
|
||||
oldHome = Paths.get(System.getProperty("user.home", "."), ".cache", "hmcl");
|
||||
}
|
||||
|
||||
if (Files.exists(oldHome)) {
|
||||
Path oldConfigPath = oldHome.resolve("config.json");
|
||||
if (Files.isRegularFile(oldConfigPath)) {
|
||||
try {
|
||||
Files.copy(oldConfigPath, GLOBAL_CONFIG_PATH);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to migrate global config", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Files.exists(GLOBAL_CONFIG_PATH)) {
|
||||
try {
|
||||
String content = FileUtils.readText(GLOBAL_CONFIG_PATH);
|
||||
|
||||
@@ -70,8 +70,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
|
||||
private IntegerProperty platformPromptVersion = new SimpleIntegerProperty();
|
||||
|
||||
private StringProperty multiplayerToken = new SimpleStringProperty();
|
||||
|
||||
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||
|
||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||
@@ -151,18 +149,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
this.multiplayerAgreementVersion.set(multiplayerAgreementVersion);
|
||||
}
|
||||
|
||||
public String getMultiplayerToken() {
|
||||
return multiplayerToken.get();
|
||||
}
|
||||
|
||||
public StringProperty multiplayerTokenProperty() {
|
||||
return multiplayerToken;
|
||||
}
|
||||
|
||||
public void setMultiplayerToken(String multiplayerToken) {
|
||||
this.multiplayerToken.set(multiplayerToken);
|
||||
}
|
||||
|
||||
public static class Serializer implements JsonSerializer<GlobalConfig>, JsonDeserializer<GlobalConfig> {
|
||||
private static final Set<String> knownFields = new HashSet<>(Arrays.asList(
|
||||
"agreementVersion",
|
||||
@@ -181,7 +167,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||
jsonObject.add("platformPromptVersion", context.serialize(src.getPlatformPromptVersion()));
|
||||
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
|
||||
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
|
||||
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||
@@ -200,7 +185,6 @@ public class GlobalConfig implements Cloneable, Observable {
|
||||
GlobalConfig config = new GlobalConfig();
|
||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setPlatformPromptVersion(Optional.ofNullable(obj.get("platformPromptVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null));
|
||||
config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.setting;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.EventBus;
|
||||
@@ -29,8 +30,8 @@ import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static javafx.collections.FXCollections.observableArrayList;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
@@ -130,8 +131,11 @@ public final class Profiles {
|
||||
if (!initialized)
|
||||
return;
|
||||
// update storage
|
||||
config().getConfigurations().clear();
|
||||
config().getConfigurations().putAll(profiles.stream().collect(Collectors.toMap(Profile::getName, it -> it)));
|
||||
TreeMap<String, Profile> newConfigurations = new TreeMap<>();
|
||||
for (Profile profile : profiles) {
|
||||
newConfigurations.put(profile.getName(), profile);
|
||||
}
|
||||
config().getConfigurations().setValue(FXCollections.observableMap(newConfigurations));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,8 @@ public enum VersionIconType {
|
||||
CRAFT_TABLE("/assets/img/craft_table.png"),
|
||||
FABRIC("/assets/img/fabric.png"),
|
||||
FORGE("/assets/img/forge.png"),
|
||||
FURNACE("/assets/img/furnace.png");
|
||||
FURNACE("/assets/img/furnace.png"),
|
||||
QUILT("/assets/img/quilt.png");
|
||||
|
||||
// Please append new items at last
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import javafx.beans.property.*;
|
||||
import org.jackhuang.hmcl.game.*;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
@@ -41,8 +42,6 @@ import java.util.Optional;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.jfoenix.concurrency.JFXUtilities.runInFX;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author huangyuhui
|
||||
@@ -660,9 +659,7 @@ public final class VersionSetting implements Cloneable {
|
||||
.filter(java -> java.getVersion().equals(getJava()))
|
||||
.collect(Collectors.toList());
|
||||
if (matchedJava.isEmpty()) {
|
||||
runInFX(() -> {
|
||||
setJava("Auto");
|
||||
});
|
||||
FXUtils.runInFX(() -> setJava("Auto"));
|
||||
return JavaVersion.fromCurrentEnvironment();
|
||||
} else {
|
||||
return matchedJava.stream()
|
||||
@@ -796,9 +793,17 @@ public final class VersionSetting implements Cloneable {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static <T> T getOrDefault(T[] values, JsonElement index, T defaultValue) {
|
||||
if (index == null)
|
||||
return defaultValue;
|
||||
|
||||
int idx = index.getAsInt();
|
||||
return idx >= 0 && idx < values.length ? values[idx] : defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public VersionSetting deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
if (json == JsonNull.INSTANCE || !(json instanceof JsonObject))
|
||||
if (!(json instanceof JsonObject))
|
||||
return null;
|
||||
JsonObject obj = (JsonObject) json;
|
||||
|
||||
@@ -829,15 +834,15 @@ public final class VersionSetting implements Cloneable {
|
||||
vs.setNotCheckJVM(Optional.ofNullable(obj.get("notCheckJVM")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setNotPatchNatives(Optional.ofNullable(obj.get("notPatchNatives")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setShowLogs(Optional.ofNullable(obj.get("showLogs")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setLauncherVisibility(LauncherVisibility.values()[Optional.ofNullable(obj.get("launcherVisibility")).map(JsonElement::getAsInt).orElse(LauncherVisibility.HIDE.ordinal())]);
|
||||
vs.setProcessPriority(ProcessPriority.values()[Optional.ofNullable(obj.get("processPriority")).map(JsonElement::getAsInt).orElse(ProcessPriority.NORMAL.ordinal())]);
|
||||
vs.setLauncherVisibility(getOrDefault(LauncherVisibility.values(), obj.get("launcherVisibility"), LauncherVisibility.HIDE));
|
||||
vs.setProcessPriority(getOrDefault(ProcessPriority.values(), obj.get("processPriority"), ProcessPriority.NORMAL));
|
||||
vs.setUseSoftwareRenderer(Optional.ofNullable(obj.get("useSoftwareRenderer")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeGLFW(Optional.ofNullable(obj.get("useNativeGLFW")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setUseNativeOpenAL(Optional.ofNullable(obj.get("useNativeOpenAL")).map(JsonElement::getAsBoolean).orElse(false));
|
||||
vs.setGameDirType(GameDirectoryType.values()[Optional.ofNullable(obj.get("gameDirType")).map(JsonElement::getAsInt).orElse(GameDirectoryType.ROOT_FOLDER.ordinal())]);
|
||||
vs.setGameDirType(getOrDefault(GameDirectoryType.values(), obj.get("gameDirType"), GameDirectoryType.ROOT_FOLDER));
|
||||
vs.setDefaultJavaPath(Optional.ofNullable(obj.get("defaultJavaPath")).map(JsonElement::getAsString).orElse(null));
|
||||
vs.setNativesDirType(NativesDirectoryType.values()[Optional.ofNullable(obj.get("nativesDirType")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setVersionIcon(VersionIconType.values()[Optional.ofNullable(obj.get("versionIcon")).map(JsonElement::getAsInt).orElse(0)]);
|
||||
vs.setNativesDirType(getOrDefault(NativesDirectoryType.values(), obj.get("nativesDirType"), NativesDirectoryType.VERSION_FOLDER));
|
||||
vs.setVersionIcon(getOrDefault(VersionIconType.values(), obj.get("versionIcon"), VersionIconType.DEFAULT));
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ public final class AwtUtils {
|
||||
|
||||
public static void setAppleIcon(Image image) {
|
||||
try {
|
||||
Class<?> taskbarClass = Class.forName("java.awt.TaskBar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskBar");
|
||||
Class<?> taskbarClass = Class.forName("java.awt.Taskbar");
|
||||
Method getTaskBarMethod = taskbarClass.getDeclaredMethod("getTaskbar");
|
||||
Object taskBar = getTaskBarMethod.invoke(null);
|
||||
Method setIconImageMethod = taskbarClass.getDeclaredMethod("setIconImage", Image.class);
|
||||
setIconImageMethod.invoke(taskBar, image);
|
||||
|
||||
@@ -45,7 +45,6 @@ import org.jackhuang.hmcl.ui.download.DownloadPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
import org.jackhuang.hmcl.ui.main.LauncherSettingsPage;
|
||||
import org.jackhuang.hmcl.ui.main.RootPage;
|
||||
import org.jackhuang.hmcl.ui.multiplayer.MultiplayerPage;
|
||||
import org.jackhuang.hmcl.ui.versions.GameListPage;
|
||||
import org.jackhuang.hmcl.ui.versions.VersionPage;
|
||||
import org.jackhuang.hmcl.util.FutureCallback;
|
||||
@@ -93,7 +92,6 @@ public final class Controllers {
|
||||
accountListPage.authServersProperty().bindContentBidirectional(config().getAuthlibInjectorServers());
|
||||
return accountListPage;
|
||||
});
|
||||
private static Lazy<MultiplayerPage> multiplayerPage = new Lazy<>(MultiplayerPage::new);
|
||||
private static Lazy<LauncherSettingsPage> settingsPage = new Lazy<>(LauncherSettingsPage::new);
|
||||
|
||||
private Controllers() {
|
||||
@@ -122,11 +120,6 @@ public final class Controllers {
|
||||
return rootPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static MultiplayerPage getMultiplayerPage() {
|
||||
return multiplayerPage.get();
|
||||
}
|
||||
|
||||
// FXThread
|
||||
public static LauncherSettingsPage getSettingsPage() {
|
||||
return settingsPage.get();
|
||||
@@ -148,10 +141,16 @@ public final class Controllers {
|
||||
}
|
||||
|
||||
public static void onApplicationStop() {
|
||||
config().setHeight(stageHeight.get());
|
||||
config().setWidth(stageWidth.get());
|
||||
stageHeight = null;
|
||||
stageWidth = null;
|
||||
if (stageHeight != null) {
|
||||
config().setHeight(stageHeight.get());
|
||||
stageHeight.unbind();
|
||||
stageHeight = null;
|
||||
}
|
||||
if (stageWidth != null) {
|
||||
config().setWidth(stageWidth.get());
|
||||
stageWidth.unbind();
|
||||
stageWidth = null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void initialize(Stage stage) {
|
||||
@@ -322,9 +321,12 @@ public final class Controllers {
|
||||
rootPage = null;
|
||||
versionPage = null;
|
||||
gameListPage = null;
|
||||
downloadPage = null;
|
||||
accountListPage = null;
|
||||
settingsPage = null;
|
||||
decorator = null;
|
||||
stage = null;
|
||||
scene = null;
|
||||
onApplicationStop();
|
||||
}
|
||||
}
|
||||
@@ -34,10 +34,8 @@ import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.image.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.image.PixelWriter;
|
||||
import javafx.scene.image.WritableImage;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.ColumnConstraints;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -49,6 +47,7 @@ import javafx.scene.text.TextFlow;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
@@ -58,6 +57,7 @@ import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.javafx.ExtendedProperties;
|
||||
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.NodeList;
|
||||
@@ -72,6 +72,7 @@ import javax.xml.parsers.ParserConfigurationException;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
@@ -357,53 +358,77 @@ public final class FXUtils {
|
||||
|
||||
public static void openFolder(File file) {
|
||||
if (!FileUtils.makeDirectory(file)) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
LOG.log(Level.SEVERE, "Unable to make directory " + file);
|
||||
return;
|
||||
}
|
||||
|
||||
String path = file.getAbsolutePath();
|
||||
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case OSX:
|
||||
String openCommand;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommand = "explorer.exe";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommand = "/usr/bin/open";
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && new File("/usr/bin/xdg-open").exists())
|
||||
openCommand = "/usr/bin/xdg-open";
|
||||
else
|
||||
openCommand = null;
|
||||
|
||||
thread(() -> {
|
||||
if (openCommand != null) {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", path});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by executing /usr/bin/open", e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommand, path);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Open " + path + " failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to open " + path + " by executing " + openCommand, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
thread(() -> {
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to java.awt.Desktop::open
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().open(file);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.SEVERE, "Unable to open " + path + " by java.awt.Desktop.getDesktop()::open", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showFileInExplorer(Path file) {
|
||||
switch (OperatingSystem.CURRENT_OS) {
|
||||
case WINDOWS:
|
||||
String path = file.toAbsolutePath().toString();
|
||||
|
||||
String[] openCommands;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS)
|
||||
openCommands = new String[]{"explorer.exe", "/select,", path};
|
||||
else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
openCommands = new String[]{"/usr/bin/open", "-R", path};
|
||||
else
|
||||
openCommands = null;
|
||||
|
||||
if (openCommands != null) {
|
||||
thread(() -> {
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"explorer.exe", "/select,", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing explorer /select," + file, e);
|
||||
int exitCode = SystemUtils.callExternalProcess(openCommands);
|
||||
|
||||
// explorer.exe always return 1
|
||||
if (exitCode == 0 || (exitCode == 1 && OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS))
|
||||
return;
|
||||
else
|
||||
LOG.warning("Show " + path + " in explorer failed with code " + exitCode);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Unable to show " + path + " in explorer", e);
|
||||
}
|
||||
break;
|
||||
case OSX:
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", "-R", file.toAbsolutePath().toString()});
|
||||
} catch (IOException e) {
|
||||
Logging.LOG.log(Level.SEVERE, "Unable to open " + file + " by executing /usr/bin/open -R " + file, e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// We do not have an universal method to show file in file manager.
|
||||
|
||||
// Fallback to open folder
|
||||
openFolder(file.getParent().toFile());
|
||||
break;
|
||||
});
|
||||
} else {
|
||||
// We do not have a universal method to show file in file manager.
|
||||
openFolder(file.getParent().toFile());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,34 +451,38 @@ public final class FXUtils {
|
||||
if (link == null)
|
||||
return;
|
||||
|
||||
if (java.awt.Desktop.isDesktopSupported()) {
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
thread(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
Runtime.getRuntime().exec(new String[]{"rundll32.exe", "url.dll,FileProtocolHandler", link});
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
LOG.log(Level.WARNING, "An exception occurred while calling rundll32", e);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
} if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
|
||||
for (String browser : linuxBrowsers) {
|
||||
try (final InputStream is = Runtime.getRuntime().exec(new String[]{"which", browser}).getInputStream()) {
|
||||
if (is.read() != -1) {
|
||||
Runtime.getRuntime().exec(new String[]{browser, link});
|
||||
return;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "No known browser found");
|
||||
}
|
||||
}
|
||||
try {
|
||||
java.awt.Desktop.getDesktop().browse(new URI(link));
|
||||
} catch (Throwable e) {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX)
|
||||
try {
|
||||
Runtime.getRuntime().exec(new String[]{"/usr/bin/open", link});
|
||||
} catch (IOException ex) {
|
||||
Logging.LOG.log(Level.WARNING, "Unable to open link: " + link, ex);
|
||||
}
|
||||
Logging.LOG.log(Level.WARNING, "Failed to open link: " + link, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void showWebDialog(String title, String content) {
|
||||
@@ -469,12 +498,12 @@ public final class FXUtils {
|
||||
} catch (NoClassDefFoundError | UnsatisfiedLinkError e) {
|
||||
LOG.log(Level.WARNING, "WebView is missing or initialization failed, use JEditorPane replaced", e);
|
||||
|
||||
SwingUtils.initLookAndFeel();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
final JFrame frame = new JFrame(title);
|
||||
frame.setSize(width, height);
|
||||
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
|
||||
frame.setLocationByPlatform(true);
|
||||
//noinspection ConstantConditions
|
||||
frame.setIconImage(new ImageIcon(FXUtils.class.getResource("/assets/img/icon.png")).getImage());
|
||||
frame.setLayout(new BorderLayout());
|
||||
|
||||
@@ -502,20 +531,85 @@ public final class FXUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().bindBidirectional(property, SafeStringConverter.fromInteger());
|
||||
public static <T> void bind(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
textField.setText(converter == null ? (String) property.getValue() : converter.toString(property.getValue()));
|
||||
TextFieldBindingListener<T> listener = new TextFieldBindingListener<>(textField, property, converter);
|
||||
textField.focusedProperty().addListener((ChangeListener<Boolean>) listener);
|
||||
property.addListener(listener);
|
||||
}
|
||||
|
||||
public static void unbindInt(JFXTextField textField, Property<Number> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void bindInt(JFXTextField textField, Property<Number> property) {
|
||||
bind(textField, property, SafeStringConverter.fromInteger());
|
||||
}
|
||||
|
||||
public static void bindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().bindBidirectional(property);
|
||||
bind(textField, property, null);
|
||||
}
|
||||
|
||||
public static void unbindString(JFXTextField textField, Property<String> property) {
|
||||
textField.textProperty().unbindBidirectional(property);
|
||||
public static void unbind(JFXTextField textField, Property<?> property) {
|
||||
TextFieldBindingListener<?> listener = new TextFieldBindingListener<>(textField, property, null);
|
||||
textField.focusedProperty().removeListener((ChangeListener<Boolean>) listener);
|
||||
property.removeListener(listener);
|
||||
}
|
||||
|
||||
private static final class TextFieldBindingListener<T> implements ChangeListener<Boolean>, InvalidationListener {
|
||||
private final int hashCode;
|
||||
private final WeakReference<JFXTextField> textFieldRef;
|
||||
private final WeakReference<Property<T>> propertyRef;
|
||||
private final StringConverter<T> converter;
|
||||
|
||||
TextFieldBindingListener(JFXTextField textField, Property<T> property, StringConverter<T> converter) {
|
||||
this.textFieldRef = new WeakReference<>(textField);
|
||||
this.propertyRef = new WeakReference<>(property);
|
||||
this.converter = converter;
|
||||
this.hashCode = System.identityHashCode(textField) ^ System.identityHashCode(property);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean focused) { // On TextField changed
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null && oldValue == Boolean.TRUE && focused == Boolean.FALSE) {
|
||||
if (textField.validate()) {
|
||||
String newText = textField.getText();
|
||||
@SuppressWarnings("unchecked")
|
||||
T newValue = converter == null ? (T) newText : converter.fromString(newText);
|
||||
|
||||
if (!Objects.equals(newValue, property.getValue()))
|
||||
property.setValue(newValue);
|
||||
} else {
|
||||
// Rollback to old value
|
||||
invalidated(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void invalidated(Observable observable) { // On property change
|
||||
JFXTextField textField = textFieldRef.get();
|
||||
Property<T> property = this.propertyRef.get();
|
||||
|
||||
if (textField != null && property != null) {
|
||||
T value = property.getValue();
|
||||
textField.setText(converter == null ? (String) value : converter.toString(value));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (!(obj instanceof TextFieldBindingListener))
|
||||
return false;
|
||||
TextFieldBindingListener<?> other = (TextFieldBindingListener<?>) obj;
|
||||
return this.hashCode == other.hashCode
|
||||
&& this.textFieldRef.get() == other.textFieldRef.get()
|
||||
&& this.propertyRef.get() == other.propertyRef.get();
|
||||
}
|
||||
}
|
||||
|
||||
public static void bindBoolean(JFXToggleButton toggleButton, Property<Boolean> property) {
|
||||
@@ -632,10 +726,17 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static <T> Callback<ListView<T>, ListCell<T>> jfxListCellFactory(Function<T, Node> graphicBuilder) {
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
return view -> new JFXListCell<T>() {
|
||||
@Override
|
||||
public void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (!empty) {
|
||||
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
|
||||
setGraphic(graphicBuilder.apply(item));
|
||||
@@ -688,11 +789,12 @@ public final class FXUtils {
|
||||
// Based on https://stackoverflow.com/a/57552025
|
||||
// Fix #874: Use it instead of SwingFXUtils.toFXImage
|
||||
public static WritableImage toFXImage(BufferedImage image) {
|
||||
WritableImage wr = new WritableImage(image.getWidth(), image.getHeight());
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
final int iw = image.getWidth();
|
||||
final int ih = image.getHeight();
|
||||
|
||||
WritableImage wr = new WritableImage(iw, ih);
|
||||
PixelWriter pw = wr.getPixelWriter();
|
||||
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
pw.setArgb(x, y, image.getRGB(x, y));
|
||||
@@ -701,6 +803,21 @@ public final class FXUtils {
|
||||
return wr;
|
||||
}
|
||||
|
||||
public static BufferedImage fromFXImage(Image image) {
|
||||
final int iw = (int) image.getWidth();
|
||||
final int ih = (int) image.getHeight();
|
||||
|
||||
PixelReader pr = image.getPixelReader();
|
||||
BufferedImage bufferedImage = new BufferedImage(iw, ih, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int x = 0; x < iw; x++) {
|
||||
for (int y = 0; y < ih; y++) {
|
||||
bufferedImage.setRGB(x, y, pr.getArgb(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
return bufferedImage;
|
||||
}
|
||||
|
||||
public static void copyText(String text) {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(text);
|
||||
@@ -710,6 +827,9 @@ public final class FXUtils {
|
||||
}
|
||||
|
||||
public static List<Node> parseSegment(String segment, Consumer<String> hyperlinkAction) {
|
||||
if (segment.indexOf('<') < 0)
|
||||
return Collections.singletonList(new Text(segment));
|
||||
|
||||
try {
|
||||
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
|
||||
DocumentBuilder builder = factory.newDocumentBuilder();
|
||||
|
||||
@@ -42,13 +42,13 @@ import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
@@ -85,9 +85,9 @@ public class GameCrashWindow extends Stage {
|
||||
private final LaunchOptions launchOptions;
|
||||
private final View view;
|
||||
|
||||
private final List<Pair<String, Log4jLevel>> logs;
|
||||
private final Collection<Pair<String, Log4jLevel>> logs;
|
||||
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List<Pair<String, Log4jLevel>> logs) {
|
||||
public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, Collection<Pair<String, Log4jLevel>> logs) {
|
||||
this.managedProcess = managedProcess;
|
||||
this.exitType = exitType;
|
||||
this.repository = repository;
|
||||
@@ -223,7 +223,7 @@ public class GameCrashWindow extends Stage {
|
||||
private void showLogWindow() {
|
||||
LogWindow logWindow = new LogWindow();
|
||||
|
||||
logWindow.logLine("Command: " + new CommandBuilder().addAll(managedProcess.getCommands()).toString(), Log4jLevel.INFO);
|
||||
logWindow.logLine(Logging.filterForbiddenToken("Command: " + new CommandBuilder().addAll(managedProcess.getCommands())), Log4jLevel.INFO);
|
||||
if (managedProcess.getClasspath() != null) logWindow.logLine("ClassPath: " + managedProcess.getClasspath(), Log4jLevel.INFO);
|
||||
for (Map.Entry<String, Log4jLevel> entry : logs)
|
||||
logWindow.logLine(entry.getKey(), entry.getValue());
|
||||
@@ -239,15 +239,11 @@ public class GameCrashWindow extends Stage {
|
||||
.thenComposeAsync(logs ->
|
||||
LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString()))
|
||||
.thenRunAsync(() -> {
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(e -> {
|
||||
LOG.log(Level.WARNING, "Failed to export game crash info", e);
|
||||
|
||||
@@ -34,23 +34,18 @@ import javafx.scene.control.Label;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.Stage;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.util.Log4jLevel;
|
||||
import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -99,8 +94,8 @@ public final class LogWindow extends Stage {
|
||||
levelShownMap.values().forEach(property -> property.addListener((a, b, newValue) -> shakeLogs()));
|
||||
}
|
||||
|
||||
public void logLine(String line, Log4jLevel level) {
|
||||
Log log = new Log(Logging.filterForbiddenToken(parseEscapeSequence(line)), level);
|
||||
public void logLine(String filteredLine, Log4jLevel level) {
|
||||
Log log = new Log(parseEscapeSequence(filteredLine), level);
|
||||
logs.add(log);
|
||||
if (levelShownMap.get(level).get())
|
||||
impl.listView.getItems().add(log);
|
||||
@@ -155,12 +150,12 @@ public final class LogWindow extends Stage {
|
||||
|
||||
public class LogWindowImpl extends Control {
|
||||
|
||||
private ListView<Log> listView = new JFXListView<>();
|
||||
private BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
private final ListView<Log> listView = new JFXListView<>();
|
||||
private final BooleanProperty autoScroll = new SimpleBooleanProperty();
|
||||
private final List<StringProperty> buttonText = IntStream.range(0, 5).mapToObj(x -> new SimpleStringProperty()).collect(Collectors.toList());
|
||||
private final List<BooleanProperty> showLevel = IntStream.range(0, 5).mapToObj(x -> new SimpleBooleanProperty(true)).collect(Collectors.toList());
|
||||
private final JFXComboBox<String> cboLines = new JFXComboBox<>();
|
||||
private final BooleanProperty showCrashReport = new SimpleBooleanProperty();
|
||||
|
||||
LogWindowImpl() {
|
||||
getStyleClass().add("log-window");
|
||||
@@ -207,13 +202,13 @@ public final class LogWindow extends Stage {
|
||||
return;
|
||||
}
|
||||
|
||||
JOptionPane.showMessageDialog(null, i18n("settings.launcher.launcher_log.export.success", logFile), i18n("settings.launcher.launcher_log.export"), JOptionPane.INFORMATION_MESSAGE);
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile));
|
||||
alert.setTitle(i18n("settings.launcher.launcher_log.export"));
|
||||
alert.showAndWait();
|
||||
});
|
||||
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -229,13 +224,13 @@ public final class LogWindow extends Stage {
|
||||
}
|
||||
|
||||
private static class LogWindowSkin extends SkinBase<LogWindowImpl> {
|
||||
private static PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||
private static final PseudoClass FATAL = PseudoClass.getPseudoClass("fatal");
|
||||
private static final PseudoClass ERROR = PseudoClass.getPseudoClass("error");
|
||||
private static final PseudoClass WARN = PseudoClass.getPseudoClass("warn");
|
||||
private static final PseudoClass INFO = PseudoClass.getPseudoClass("info");
|
||||
private static final PseudoClass DEBUG = PseudoClass.getPseudoClass("debug");
|
||||
private static final PseudoClass TRACE = PseudoClass.getPseudoClass("trace");
|
||||
|
||||
private static ToggleButton createToggleButton(String backgroundColor, StringProperty buttonText, BooleanProperty showLevel) {
|
||||
ToggleButton button = new ToggleButton();
|
||||
@@ -292,6 +287,7 @@ public final class LogWindow extends Stage {
|
||||
listView.scrollTo(listView.getItems().size() - 1);
|
||||
});
|
||||
listView.setStyle("-fx-font-family: " + config().getFontFamily() + "; -fx-font-size: " + config().getFontSize() + "px;");
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new ListCell<Log>() {
|
||||
{
|
||||
getStyleClass().add("log-window-list-cell");
|
||||
@@ -308,6 +304,12 @@ public final class LogWindow extends Stage {
|
||||
@Override
|
||||
protected void updateItem(Log item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
pseudoClassStateChanged(EMPTY, empty);
|
||||
pseudoClassStateChanged(FATAL, !empty && item.level == Log4jLevel.FATAL);
|
||||
pseudoClassStateChanged(ERROR, !empty && item.level == Log4jLevel.ERROR);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.jackhuang.hmcl.ui;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
public final class SwingUtils {
|
||||
static {
|
||||
if (System.getProperty("swing.defaultlaf") == null) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private SwingUtils() {
|
||||
}
|
||||
|
||||
public static void initLookAndFeel() {
|
||||
// Make sure the static constructor is called
|
||||
}
|
||||
|
||||
public static void showInfoDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showWarningDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
|
||||
public static void showErrorDialog(Object message) {
|
||||
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ public class UpgradeDialog extends JFXDialogLayout {
|
||||
}
|
||||
|
||||
{
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html";
|
||||
String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html#nowchange";
|
||||
try {
|
||||
WebView webView = new WebView();
|
||||
webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile());
|
||||
|
||||
@@ -24,11 +24,11 @@ import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class WeakListenerHolder {
|
||||
private List<Object> refs = new LinkedList<>();
|
||||
private final List<Object> refs = new ArrayList<>(0);
|
||||
|
||||
public WeakListenerHolder() {
|
||||
}
|
||||
|
||||
@@ -22,9 +22,8 @@ import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
@@ -34,20 +33,18 @@ import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
|
||||
import static javafx.beans.binding.Bindings.createStringBinding;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getAccountFactory;
|
||||
import static org.jackhuang.hmcl.setting.Accounts.getLocalizedLoginTypeName;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.toFXImage;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
private final Tooltip tooltip;
|
||||
private final ImageView imageView;
|
||||
private final Canvas canvas;
|
||||
|
||||
private ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
|
||||
|
||||
@Override
|
||||
protected void invalidated() {
|
||||
@@ -55,17 +52,19 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
if (account == null) {
|
||||
titleProperty().unbind();
|
||||
subtitleProperty().unbind();
|
||||
imageView.imageProperty().unbind();
|
||||
tooltip.textProperty().unbind();
|
||||
setTitle(i18n("account.missing"));
|
||||
setSubtitle(i18n("account.missing.add"));
|
||||
imageView.setImage(toFXImage(TexturesLoader.toAvatar(TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage(), 32)));
|
||||
tooltip.setText(i18n("account.create"));
|
||||
|
||||
TexturesLoader.unbindAvatar(canvas);
|
||||
TexturesLoader.drawAvatar(canvas, TexturesLoader.getDefaultSkin(TextureModel.STEVE).getImage());
|
||||
|
||||
} else {
|
||||
titleProperty().bind(BindingMapping.of(account, Account::getCharacter));
|
||||
subtitleProperty().bind(accountSubtitle(account));
|
||||
imageView.imageProperty().bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
tooltip.textProperty().bind(accountTooltip(account));
|
||||
TexturesLoader.bindAvatar(canvas, account);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -74,9 +73,8 @@ public class AccountAdvancedListItem extends AdvancedListItem {
|
||||
tooltip = new Tooltip();
|
||||
FXUtils.installFastTooltip(this, tooltip);
|
||||
|
||||
Pair<Node, ImageView> view = createImageView(null);
|
||||
setLeftGraphic(view.getKey());
|
||||
imageView = view.getValue();
|
||||
canvas = new Canvas(32, 32);
|
||||
setLeftGraphic(canvas);
|
||||
|
||||
setActionButtonVisible(false);
|
||||
|
||||
|
||||
@@ -20,14 +20,11 @@ package org.jackhuang.hmcl.ui.account;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.ObjectBinding;
|
||||
import javafx.beans.binding.StringBinding;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.scene.control.RadioButton;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.stage.FileChooser;
|
||||
import org.jackhuang.hmcl.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.AuthenticationException;
|
||||
@@ -39,7 +36,6 @@ import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.CompleteGameProfile;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.TextureType;
|
||||
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@@ -70,7 +66,6 @@ public class AccountListItem extends RadioButton {
|
||||
private final Account account;
|
||||
private final StringProperty title = new SimpleStringProperty();
|
||||
private final StringProperty subtitle = new SimpleStringProperty();
|
||||
private final ObjectProperty<Image> image = new SimpleObjectProperty<>();
|
||||
|
||||
public AccountListItem(Account account) {
|
||||
this.account = account;
|
||||
@@ -95,8 +90,6 @@ public class AccountListItem extends RadioButton {
|
||||
account.getUsername().isEmpty() ? characterName :
|
||||
Bindings.concat(account.getUsername(), " - ", characterName));
|
||||
}
|
||||
|
||||
image.bind(TexturesLoader.fxAvatarBinding(account, 32));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -226,16 +219,4 @@ public class AccountListItem extends RadioButton {
|
||||
public StringProperty subtitleProperty() {
|
||||
return subtitle;
|
||||
}
|
||||
|
||||
public Image getImage() {
|
||||
return image.get();
|
||||
}
|
||||
|
||||
public void setImage(Image image) {
|
||||
this.image.set(image);
|
||||
}
|
||||
|
||||
public ObjectProperty<Image> imageProperty() {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
@@ -21,15 +21,16 @@ import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXRadioButton;
|
||||
import com.jfoenix.effects.JFXDepthManager;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.SkinBase;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.game.TexturesLoader;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -64,9 +65,8 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
center.setSpacing(8);
|
||||
center.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
ImageView imageView = new ImageView();
|
||||
FXUtils.limitSize(imageView, 32, 32);
|
||||
imageView.imageProperty().bind(skinnable.imageProperty());
|
||||
Canvas canvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(canvas, skinnable.getAccount());
|
||||
|
||||
Label title = new Label();
|
||||
title.getStyleClass().add("title");
|
||||
@@ -84,7 +84,7 @@ public class AccountListItemSkin extends SkinBase<AccountListItem> {
|
||||
item.getStyleClass().add("two-line-list-item");
|
||||
BorderPane.setAlignment(item, Pos.CENTER);
|
||||
|
||||
center.getChildren().setAll(imageView, item);
|
||||
center.getChildren().setAll(canvas, item);
|
||||
root.setCenter(center);
|
||||
|
||||
HBox right = new HBox();
|
||||
|
||||
@@ -45,8 +45,10 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
|
||||
|
||||
@@ -148,7 +150,13 @@ public class AccountListPage extends DecoratorAnimatedPage implements DecoratorP
|
||||
|
||||
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
|
||||
item.titleProperty().bind(title);
|
||||
item.subtitleProperty().set(URI.create(server.getUrl()).getHost());
|
||||
String host = "";
|
||||
try {
|
||||
host = URI.create(server.getUrl()).getHost();
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOG.log(Level.WARNING, "Unparsable authlib-injector server url " + server.getUrl(), e);
|
||||
}
|
||||
item.subtitleProperty().set(host);
|
||||
Tooltip tooltip = new Tooltip();
|
||||
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
|
||||
FXUtils.installFastTooltip(item, tooltip);
|
||||
|
||||
@@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXDialogLayout;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -67,6 +68,7 @@ public class AddAuthlibInjectorServerPane extends StackPane implements DialogAwa
|
||||
loadFXML(this, "/assets/fxml/authlib-injector-server-add.fxml");
|
||||
root.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
|
||||
|
||||
lblCreationWarning.maxWidthProperty().bind(((FlowPane) lblCreationWarning.getParent()).widthProperty());
|
||||
btnAddNext.disableProperty().bind(txtServerUrl.textProperty().isEmpty());
|
||||
nextPane.hideSpinner();
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.TextInputControl;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.*;
|
||||
import org.jackhuang.hmcl.auth.AccountFactory;
|
||||
import org.jackhuang.hmcl.auth.CharacterSelector;
|
||||
@@ -295,10 +295,12 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
JFXHyperlink deauthorizeLink = new JFXHyperlink(i18n("account.methods.microsoft.deauthorize"));
|
||||
deauthorizeLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/consent/Edit?client_id=000000004C794E0A"));
|
||||
JFXHyperlink forgotpasswordLink = new JFXHyperlink(i18n("account.methods.forgot_password"));
|
||||
forgotpasswordLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/password/forgot"));
|
||||
JFXHyperlink createProfileLink = new JFXHyperlink(i18n("account.methods.microsoft.makegameidsettings"));
|
||||
createProfileLink.setOnAction(e -> FXUtils.openLink("https://www.minecraft.net/msaprofile/mygames/editprofile"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, createProfileLink);
|
||||
box.getChildren().setAll(profileLink, birthLink, purchaseLink, deauthorizeLink, forgotpasswordLink, createProfileLink);
|
||||
GridPane.setColumnSpan(box, 2);
|
||||
|
||||
vbox.getChildren().setAll(hintPane, box);
|
||||
@@ -506,6 +508,8 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
}
|
||||
|
||||
if (factory instanceof OfflineAccountFactory) {
|
||||
txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters"));
|
||||
|
||||
JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.yggdrasil.purchase"));
|
||||
purchaseLink.setOnAction(e -> FXUtils.openLink(YggdrasilService.PURCHASE_URL));
|
||||
HBox linkPane = new HBox(purchaseLink);
|
||||
@@ -650,12 +654,10 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
|
||||
public GameProfile select(YggdrasilService service, List<GameProfile> profiles) throws NoSelectedCharacterException {
|
||||
Platform.runLater(() -> {
|
||||
for (GameProfile profile : profiles) {
|
||||
ImageView portraitView = new ImageView();
|
||||
portraitView.setSmooth(false);
|
||||
portraitView.imageProperty().bind(TexturesLoader.fxAvatarBinding(service, profile.getId(), 32));
|
||||
FXUtils.limitSize(portraitView, 32, 32);
|
||||
Canvas portraitCanvas = new Canvas(32, 32);
|
||||
TexturesLoader.bindAvatar(portraitCanvas, service, profile.getId());
|
||||
|
||||
IconedItem accountItem = new IconedItem(portraitView, profile.getName());
|
||||
IconedItem accountItem = new IconedItem(portraitCanvas, profile.getName());
|
||||
accountItem.setOnMouseClicked(e -> {
|
||||
selectedProfile = profile;
|
||||
latch.countDown();
|
||||
|
||||
@@ -22,6 +22,7 @@ import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
|
||||
public abstract class MDListCell<T> extends ListCell<T> {
|
||||
@@ -29,8 +30,11 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
|
||||
private final StackPane container = new StackPane();
|
||||
private final StackPane root = new StackPane();
|
||||
private final MutableObject<Object> lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView, MutableObject<Object> lastCell) {
|
||||
this.lastCell = lastCell;
|
||||
|
||||
public MDListCell(JFXListView<T> listView) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
|
||||
@@ -50,6 +54,14 @@ public abstract class MDListCell<T> extends ListCell<T> {
|
||||
@Override
|
||||
protected void updateItem(T item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (lastCell != null) {
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
}
|
||||
|
||||
updateControl(item, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
|
||||
@@ -23,8 +23,12 @@ import javafx.fxml.FXML;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextFlow;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -69,7 +73,17 @@ public final class MessageDialogPane extends StackPane {
|
||||
public MessageDialogPane(@NotNull String text, @Nullable String title, @NotNull MessageType type) {
|
||||
FXUtils.loadFXML(this, "/assets/fxml/message-dialog.fxml");
|
||||
|
||||
content.getChildren().setAll(FXUtils.segmentToTextFlow(text, Controllers::onHyperlinkAction));
|
||||
EnhancedTextFlow textFlow = new EnhancedTextFlow();
|
||||
textFlow.getChildren().setAll(FXUtils.parseSegment(text, Controllers::onHyperlinkAction));
|
||||
|
||||
if (textFlow.computePrefHeight(400.0) <= 350.0)
|
||||
content.getChildren().setAll(textFlow);
|
||||
else {
|
||||
ScrollPane scrollPane = new ScrollPane(textFlow);
|
||||
VBox.setVgrow(scrollPane, Priority.ALWAYS);
|
||||
scrollPane.setFitToWidth(true);
|
||||
content.getChildren().setAll(scrollPane);
|
||||
}
|
||||
|
||||
if (title != null)
|
||||
this.title.setText(title);
|
||||
@@ -114,6 +128,13 @@ public final class MessageDialogPane extends StackPane {
|
||||
return cancelButton;
|
||||
}
|
||||
|
||||
private static final class EnhancedTextFlow extends TextFlow {
|
||||
@Override
|
||||
public double computePrefHeight(double width) {
|
||||
return super.computePrefHeight(width);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private final MessageDialogPane dialog;
|
||||
|
||||
|
||||
@@ -163,10 +163,10 @@ public class DecoratorController {
|
||||
config().backgroundImageUrlProperty()));
|
||||
}
|
||||
|
||||
private Image defaultBackground = newImage("/assets/img/background.jpg");
|
||||
private Image defaultBackground;
|
||||
|
||||
/**
|
||||
* Load background image from bg/, background.png, background.jpg
|
||||
* Load background image from bg/, background.png, background.jpg, background.gif
|
||||
*/
|
||||
private Image loadDefaultBackgroundImage() {
|
||||
Optional<Image> image = randomImageIn(Paths.get("bg"));
|
||||
@@ -176,7 +176,15 @@ public class DecoratorController {
|
||||
if (!image.isPresent()) {
|
||||
image = tryLoadImage(Paths.get("background.jpg"));
|
||||
}
|
||||
return image.orElse(defaultBackground);
|
||||
if (!image.isPresent()) {
|
||||
image = tryLoadImage(Paths.get("background.gif"));
|
||||
}
|
||||
|
||||
return image.orElseGet(() -> {
|
||||
if (defaultBackground == null)
|
||||
defaultBackground = newImage("/assets/img/background.jpg");
|
||||
return defaultBackground;
|
||||
});
|
||||
}
|
||||
|
||||
private Optional<Image> randomImageIn(Path imageDir) {
|
||||
@@ -190,7 +198,7 @@ public class DecoratorController {
|
||||
.filter(Files::isReadable)
|
||||
.filter(it -> {
|
||||
String ext = getExtension(it).toLowerCase();
|
||||
return ext.equals("png") || ext.equals("jpg");
|
||||
return ext.equals("png") || ext.equals("jpg") || ext.equals("gif");
|
||||
})
|
||||
.collect(toList());
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
import org.jackhuang.hmcl.ui.animation.TransitionPane;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.construct.TabControl;
|
||||
import org.jackhuang.hmcl.ui.construct.TabHeader;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
@@ -128,19 +129,19 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
item.setTitle(i18n("resourcepack"));
|
||||
item.setLeftGraphic(wrap(SVG::textureBox));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(resourcePackTab));
|
||||
item.setOnAction(e -> tab.select(resourcePackTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(resourcePackTab));
|
||||
})
|
||||
// .addNavigationDrawerItem(item -> {
|
||||
// item.setTitle(i18n("download.curseforge.customization"));
|
||||
// item.setLeftGraphic(wrap(SVG::script));
|
||||
// item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(customizationTab));
|
||||
// item.setOnAction(e -> tab.select(customizationTab));
|
||||
// item.setOnAction(e -> selectTabIfCurseForgeAvailable(customizationTab));
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("world"));
|
||||
item.setLeftGraphic(wrap(SVG::earth));
|
||||
item.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(worldTab));
|
||||
item.setOnAction(e -> tab.select(worldTab));
|
||||
item.setOnAction(e -> selectTabIfCurseForgeAvailable(worldTab));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
@@ -149,7 +150,14 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
setCenter(transitionPane);
|
||||
}
|
||||
|
||||
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
private void selectTabIfCurseForgeAvailable(TabControl.Tab<?> newTab) {
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
tab.select(newTab);
|
||||
else
|
||||
Controllers.dialog(i18n("download.curseforge.unavailable"));
|
||||
}
|
||||
|
||||
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
|
||||
return () -> {
|
||||
T node = nodeSupplier.get();
|
||||
if (node instanceof VersionPage.VersionLoadable) {
|
||||
@@ -159,7 +167,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
};
|
||||
}
|
||||
|
||||
private void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
private static void download(Profile profile, @Nullable String version, RemoteMod.Version file, String subdirectoryName) {
|
||||
if (version == null) version = profile.getSelectedVersion();
|
||||
|
||||
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version).toPath() : profile.getRepository().getBaseDirectory().toPath();
|
||||
@@ -234,7 +242,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
|
||||
tab.select(worldTab);
|
||||
}
|
||||
|
||||
private class DownloadNavigator implements Navigation {
|
||||
private static final class DownloadNavigator implements Navigation {
|
||||
private final Map<String, Object> settings = new HashMap<>();
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.download.DownloadProvider;
|
||||
import org.jackhuang.hmcl.download.RemoteVersion;
|
||||
import org.jackhuang.hmcl.download.VersionList;
|
||||
@@ -40,7 +41,10 @@ import org.jackhuang.hmcl.download.forge.ForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.liteloader.LiteLoaderRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
|
||||
@@ -52,9 +56,9 @@ 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;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -122,86 +126,9 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
|
||||
btnRefresh.setGraphic(wrap(SVG.refresh(Theme.blackFillBinding(), -1, -1)));
|
||||
|
||||
list.setCellFactory(listView -> new ListCell<RemoteVersion>() {
|
||||
IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
StackPane pane = new StackPane();
|
||||
|
||||
{
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle("");
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(new Image("/assets/img/grass.png", 32, 32, false, true));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(new Image("/assets/img/craft_table.png", 32, 32, false, true));
|
||||
break;
|
||||
}
|
||||
} else if (remoteVersion instanceof LiteLoaderRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/chicken.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof OptiFineRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/command.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof ForgeRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/forge.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
} else if (remoteVersion instanceof FabricAPIRemoteVersion) {
|
||||
content.setImage(new Image("/assets/img/fabric.png", 32, 32, false, true));
|
||||
if (StringUtils.isNotBlank(content.getSubtitle())) {
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
} else {
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
MutableObject<RemoteVersionListCell> lastCell = new MutableObject<>();
|
||||
EnumMap<VersionIconType, Image> icons = new EnumMap<>(VersionIconType.class);
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, icons));
|
||||
|
||||
list.setOnMouseClicked(e -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
@@ -290,4 +217,86 @@ public final class VersionsPage extends BorderPane implements WizardPage, Refres
|
||||
private void onSponsor() {
|
||||
HMCLService.openRedirectLink("bmclapi_sponsor");
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
final StackPane pane = new StackPane();
|
||||
|
||||
private final MutableObject<RemoteVersionListCell> lastCell;
|
||||
private final EnumMap<VersionIconType, Image> icons;
|
||||
|
||||
RemoteVersionListCell(MutableObject<RemoteVersionListCell> lastCell, EnumMap<VersionIconType, Image> icons) {
|
||||
this.lastCell = lastCell;
|
||||
this.icons = icons;
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
}
|
||||
|
||||
private Image getIcon(VersionIconType type) {
|
||||
return icons.computeIfAbsent(type, iconType -> new Image(iconType.getResourceUrl(), 32, 32, false, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateItem(RemoteVersion remoteVersion, boolean empty) {
|
||||
super.updateItem(remoteVersion, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (empty) {
|
||||
setGraphic(null);
|
||||
return;
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(remoteVersion.getSelfVersion());
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(Locales.DATE_TIME_FORMATTER.get().format(remoteVersion.getReleaseDate().toInstant()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
switch (remoteVersion.getVersionType()) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(getIcon(VersionIconType.GRASS));
|
||||
break;
|
||||
case SNAPSHOT:
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(getIcon(VersionIconType.COMMAND));
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(getIcon(VersionIconType.CRAFT_TABLE));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
iconType = VersionIconType.CHICKEN;
|
||||
else if (remoteVersion instanceof OptiFineRemoteVersion)
|
||||
iconType = VersionIconType.COMMAND;
|
||||
else if (remoteVersion instanceof ForgeRemoteVersion)
|
||||
iconType = VersionIconType.FORGE;
|
||||
else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion)
|
||||
iconType = VersionIconType.FABRIC;
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = null;
|
||||
|
||||
content.setImage(iconType != null ? getIcon(iconType) : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,10 @@ public final class ExportWizardProvider implements WizardProvider {
|
||||
if (background_jpg.isFile())
|
||||
zip.putFile(background_jpg, "background.jpg");
|
||||
|
||||
File background_gif = new File("background.gif").getAbsoluteFile();
|
||||
if (background_gif.isFile())
|
||||
zip.putFile(background_gif, "background.gif");
|
||||
|
||||
zip.putFile(launcherJar.get(), launcherJar.get().getFileName().toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,19 +92,13 @@ public class AboutPage extends StackPane {
|
||||
mcmod.setSubtitle(i18n("about.thanks_to.mcmod.statement"));
|
||||
mcmod.setExternalLink("https://www.mcmod.cn/");
|
||||
|
||||
IconedTwoLineListItem noin = new IconedTwoLineListItem();
|
||||
noin.setImage(new Image("/assets/img/noin.png", 32, 32, false, true));
|
||||
noin.setTitle(i18n("about.thanks_to.noin"));
|
||||
noin.setSubtitle(i18n("about.thanks_to.noin.statement"));
|
||||
noin.setExternalLink("https://mcer.cn/cato");
|
||||
|
||||
IconedTwoLineListItem contributors = new IconedTwoLineListItem();
|
||||
contributors.setImage(new Image("/assets/img/github.png", 32, 32, false, true));
|
||||
contributors.setTitle(i18n("about.thanks_to.contributors"));
|
||||
contributors.setSubtitle(i18n("about.thanks_to.contributors.statement"));
|
||||
contributors.setExternalLink("https://github.com/huanghongxun/HMCL/graphs/contributors");
|
||||
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, noin, gamerteam, redLnn, contributors);
|
||||
thanks.getContent().setAll(yushijinhun, bangbang93, glavo, mcbbs, mcmod, gamerteam, redLnn, contributors);
|
||||
}
|
||||
|
||||
ComponentList community = new ComponentList();
|
||||
@@ -134,17 +128,17 @@ public class AboutPage extends StackPane {
|
||||
{
|
||||
IconedTwoLineListItem javafx = new IconedTwoLineListItem();
|
||||
javafx.setTitle("JavaFX");
|
||||
javafx.setSubtitle("Copyright (c) 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
|
||||
javafx.setSubtitle("Copyright © 2013, 2021, Oracle and/or its affiliates.\nLicensed under the GPL 2 with Classpath Exception.");
|
||||
javafx.setExternalLink("https://openjfx.io/");
|
||||
|
||||
IconedTwoLineListItem jfoenix = new IconedTwoLineListItem();
|
||||
jfoenix.setTitle("JFoenix");
|
||||
jfoenix.setSubtitle("Copyright (c) 2016 JFoenix.\nLicensed under the MIT License.");
|
||||
jfoenix.setSubtitle("Copyright © 2016 JFoenix.\nLicensed under the MIT License.");
|
||||
jfoenix.setExternalLink("http://www.jfoenix.com/");
|
||||
|
||||
IconedTwoLineListItem gson = new IconedTwoLineListItem();
|
||||
gson.setTitle("Gson");
|
||||
gson.setSubtitle("Copyright 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
|
||||
gson.setSubtitle("Copyright © 2008 Google Inc.\nLicensed under the Apache 2.0 License.");
|
||||
gson.setExternalLink("https://github.com/google/gson");
|
||||
|
||||
IconedTwoLineListItem xz = new IconedTwoLineListItem();
|
||||
@@ -154,27 +148,27 @@ public class AboutPage extends StackPane {
|
||||
|
||||
IconedTwoLineListItem fxgson = new IconedTwoLineListItem();
|
||||
fxgson.setTitle("fx-gson");
|
||||
fxgson.setSubtitle("Copyright (c) 2016 Joffrey Bion.\nLicensed under the MIT License.");
|
||||
fxgson.setSubtitle("Copyright © 2016 Joffrey Bion.\nLicensed under the MIT License.");
|
||||
fxgson.setExternalLink("https://github.com/joffrey-bion/fx-gson");
|
||||
|
||||
IconedTwoLineListItem constantPoolScanner = new IconedTwoLineListItem();
|
||||
constantPoolScanner.setTitle("Constant Pool Scanner");
|
||||
constantPoolScanner.setSubtitle("Copyright 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
|
||||
constantPoolScanner.setSubtitle("Copyright © 1997-2010 Oracle and/or its affiliates.\nLicensed under the GPL 2 or the CDDL.");
|
||||
constantPoolScanner.setExternalLink("https://github.com/jenkinsci/constant-pool-scanner");
|
||||
|
||||
IconedTwoLineListItem openNBT = new IconedTwoLineListItem();
|
||||
openNBT.setTitle("OpenNBT");
|
||||
openNBT.setSubtitle("Copyright (C) 2013-2021 Steveice10.\nLicensed under the MIT License.");
|
||||
openNBT.setSubtitle("Copyright © 2013-2021 Steveice10.\nLicensed under the MIT License.");
|
||||
openNBT.setExternalLink("https://github.com/Steveice10/OpenNBT");
|
||||
|
||||
IconedTwoLineListItem jstun = new IconedTwoLineListItem();
|
||||
jstun.setTitle("JSTUN");
|
||||
jstun.setSubtitle("Copyright (c) 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
|
||||
jstun.setSubtitle("Copyright © 2005 Thomas King <king@t-king.de>.\nLicensed under the Apache License 2.0.");
|
||||
jstun.setExternalLink("https://github.com/tking/JSTUN");
|
||||
|
||||
IconedTwoLineListItem minecraftJFXSkin = new IconedTwoLineListItem();
|
||||
minecraftJFXSkin.setTitle("minecraft-jfx-skin");
|
||||
minecraftJFXSkin.setSubtitle("Copyright (c) 2016 InfinityStudio.\nLicensed under the GPL 3.");
|
||||
minecraftJFXSkin.setSubtitle("Copyright © 2016 InfinityStudio.\nLicensed under the GPL 3.");
|
||||
minecraftJFXSkin.setExternalLink("https://github.com/InfinityStudio/minecraft-jfx-skin");
|
||||
|
||||
dep.getContent().setAll(javafx, jfoenix, gson, xz, fxgson, constantPoolScanner, openNBT, jstun, minecraftJFXSkin);
|
||||
|
||||
@@ -132,7 +132,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
|
||||
JFXTextField threadsField = new JFXTextField();
|
||||
FXUtils.setLimitWidth(threadsField, 60);
|
||||
threadsField.textProperty().bindBidirectional(config().downloadThreadsProperty(), SafeStringConverter.fromInteger());
|
||||
FXUtils.bindInt(threadsField, config().downloadThreadsProperty());
|
||||
|
||||
AtomicBoolean changedByTextField = new AtomicBoolean(false);
|
||||
FXUtils.onChangeAndOperate(config().downloadThreadsProperty(), value -> {
|
||||
@@ -213,7 +213,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
GridPane.setRowIndex(txtProxyHost, 1);
|
||||
GridPane.setColumnIndex(txtProxyHost, 1);
|
||||
gridPane.getChildren().add(txtProxyHost);
|
||||
txtProxyHost.textProperty().bindBidirectional(config().proxyHostProperty());
|
||||
FXUtils.bindString(txtProxyHost, config().proxyHostProperty());
|
||||
txtProxyHost.getValidators().setAll(new NumberValidator(i18n("input.number"), false));
|
||||
}
|
||||
|
||||
@@ -232,11 +232,10 @@ public class DownloadSettingsPage extends StackPane {
|
||||
FXUtils.setValidateWhileTextChanged(txtProxyPort, true);
|
||||
gridPane.getChildren().add(txtProxyPort);
|
||||
|
||||
txtProxyPort.textProperty().bindBidirectional(config().proxyPortProperty(),
|
||||
SafeStringConverter.fromInteger()
|
||||
.restrict(it -> it >= 0 && it <= 0xFFFF)
|
||||
.fallbackTo(0)
|
||||
.asPredicate(Validator.addTo(txtProxyPort)));
|
||||
FXUtils.bind(txtProxyPort, config().proxyPortProperty(), SafeStringConverter.fromInteger()
|
||||
.restrict(it -> it >= 0 && it <= 0xFFFF)
|
||||
.fallbackTo(0)
|
||||
.asPredicate(Validator.addTo(txtProxyPort)));
|
||||
}
|
||||
proxyPane.getChildren().add(gridPane);
|
||||
}
|
||||
@@ -273,7 +272,7 @@ public class DownloadSettingsPage extends StackPane {
|
||||
GridPane.setRowIndex(txtProxyUsername, 0);
|
||||
GridPane.setColumnIndex(txtProxyUsername, 1);
|
||||
authPane.getChildren().add(txtProxyUsername);
|
||||
txtProxyUsername.textProperty().bindBidirectional(config().proxyUserProperty());
|
||||
FXUtils.bindString(txtProxyUsername, config().proxyUserProperty());
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -30,6 +30,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.OAuthServer;
|
||||
import org.jackhuang.hmcl.setting.Accounts;
|
||||
@@ -117,7 +118,8 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
JFXListView<FeedbackResponse> listView = new JFXListView<>();
|
||||
spinnerPane.setContent(listView);
|
||||
Bindings.bindContent(listView.getItems(), feedbacks);
|
||||
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) {
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView, lastCell) {
|
||||
private final TwoLineListItem content = new TwoLineListItem();
|
||||
private final JFXButton likeButton = new JFXButton();
|
||||
private final JFXButton unlikeButton = new JFXButton();
|
||||
@@ -230,7 +232,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
Controllers.dialog(new AddFeedbackDialog());
|
||||
}
|
||||
|
||||
private class LoginDialog extends JFXDialogLayout {
|
||||
private static final class LoginDialog extends JFXDialogLayout {
|
||||
private final SpinnerPane spinnerPane = new SpinnerPane();
|
||||
private final Label errorLabel = new Label();
|
||||
private final BooleanProperty logging = new SimpleBooleanProperty();
|
||||
@@ -241,10 +243,7 @@ public class FeedbackPage extends VBox implements PageAware {
|
||||
VBox vbox = new VBox(8);
|
||||
setBody(vbox);
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging ->
|
||||
logging
|
||||
? i18n("account.hmcl.hint")
|
||||
: i18n("account.hmcl.hint")));
|
||||
hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> i18n("account.hmcl.hint")));
|
||||
hintPane.setOnMouseClicked(e -> {
|
||||
if (logging.get() && OAuthServer.lastlyOpenedURL != null) {
|
||||
FXUtils.copyText(OAuthServer.lastlyOpenedURL);
|
||||
|
||||
@@ -63,7 +63,7 @@ public class HelpPage extends SpinnerPane {
|
||||
|
||||
private void loadHelp() {
|
||||
showSpinner();
|
||||
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/help/").getJson(new TypeToken<List<HelpCategory>>() {
|
||||
Task.<List<HelpCategory>>supplyAsync(() -> HttpRequest.GET("https://docs.hmcl.net/index.json").getJson(new TypeToken<List<HelpCategory>>() {
|
||||
}.getType()))
|
||||
.thenAcceptAsync(Schedulers.javafx(), helpCategories -> {
|
||||
for (HelpCategory category : helpCategories) {
|
||||
|
||||
@@ -57,7 +57,10 @@ import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateHandler;
|
||||
import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.IntStream;
|
||||
@@ -89,7 +92,16 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
titleNode.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
ImageView titleIcon = new ImageView();
|
||||
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
|
||||
if (JavaVersion.CURRENT_JAVA.getParsedVersion() < 9) {
|
||||
// JavaFX 8 has some problems with @2x images
|
||||
// Force load the original icon
|
||||
try (InputStream is = MainPage.class.getResourceAsStream("/assets/img/icon.png")) {
|
||||
titleIcon.setImage(new Image(is, 20, 20, false, false));
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
} else {
|
||||
titleIcon.setImage(new Image("/assets/img/icon.png", 20, 20, false, false));
|
||||
}
|
||||
|
||||
Label titleLabel = new Label(Metadata.FULL_TITLE);
|
||||
titleLabel.getStyleClass().add("jfx-decorator-title");
|
||||
|
||||
@@ -146,12 +146,10 @@ public class PersonalizationPage extends StackPane {
|
||||
|
||||
JFXTextField txtLogFontSize = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtLogFontSize, 50);
|
||||
txtLogFontSize.textProperty().bindBidirectional(config().fontSizeProperty(),
|
||||
SafeStringConverter.fromFiniteDouble()
|
||||
.restrict(it -> it > 0)
|
||||
.fallbackTo(12.0)
|
||||
.asPredicate(Validator.addTo(txtLogFontSize)));
|
||||
|
||||
FXUtils.bind(txtLogFontSize, config().fontSizeProperty(), SafeStringConverter.fromFiniteDouble()
|
||||
.restrict(it -> it > 0)
|
||||
.fallbackTo(12.0)
|
||||
.asPredicate(Validator.addTo(txtLogFontSize)));
|
||||
|
||||
hBox.getChildren().setAll(cboLogFont, txtLogFontSize);
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
|
||||
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
|
||||
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
|
||||
@@ -153,7 +155,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
|
||||
multiplayerItem.setActionButtonVisible(false);
|
||||
multiplayerItem.setTitle(i18n("multiplayer"));
|
||||
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
|
||||
JFXHyperlink link = new JFXHyperlink(i18n("multiplayer.hint.details"));
|
||||
link.setOnAction(e -> FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/multiplayer-migrate"));
|
||||
multiplayerItem.setOnAction(e -> Controllers.dialog(
|
||||
new MessageDialogPane.Builder(i18n("multiplayer.hint"), null, MessageDialogPane.MessageType.INFO)
|
||||
.addAction(link)
|
||||
.ok(null)
|
||||
.build()));
|
||||
|
||||
// sixth item in left sidebar
|
||||
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
|
||||
|
||||
@@ -35,7 +35,6 @@ import org.jackhuang.hmcl.util.Logging;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
@@ -139,12 +138,7 @@ public final class SettingsPage extends SettingsView {
|
||||
}
|
||||
|
||||
Platform.runLater(() -> Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", logFile)));
|
||||
if (Desktop.isDesktopSupported()) {
|
||||
try {
|
||||
Desktop.getDesktop().open(logFile.toFile());
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
FXUtils.showFileInExplorer(logFile);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.scene.Cursor;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
@@ -84,10 +85,17 @@ public class SponsorPage extends StackPane {
|
||||
StackPane pane = new StackPane();
|
||||
pane.getStyleClass().add("card");
|
||||
listView = new JFXListView<>();
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() {
|
||||
@Override
|
||||
public void updateItem(Sponsor item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
// https://mail.openjdk.org/pipermail/openjfx-dev/2022-July/034764.html
|
||||
if (this == lastCell.getValue() && !isVisible())
|
||||
return;
|
||||
lastCell.setValue(this);
|
||||
|
||||
if (!empty) {
|
||||
setText(item.getName());
|
||||
setGraphic(null);
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2022 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.multiplayer;
|
||||
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ServerSocketChannel;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.nio.channels.UnresolvedAddressException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class LocalServerBroadcaster implements AutoCloseable {
|
||||
private final String address;
|
||||
private final ThreadGroup threadGroup = new ThreadGroup("JoinSession");
|
||||
|
||||
private final EventManager<Event> onExit = new EventManager<>();
|
||||
|
||||
private boolean running = true;
|
||||
|
||||
public LocalServerBroadcaster(String address) {
|
||||
this.address = address;
|
||||
this.threadGroup.setDaemon(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
running = false;
|
||||
threadGroup.interrupt();
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public EventManager<Event> onExit() {
|
||||
return onExit;
|
||||
}
|
||||
|
||||
public static final Pattern ADDRESS_PATTERN = Pattern.compile("^\\s*(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d{1,5})\\s*$");
|
||||
|
||||
public void start() {
|
||||
Thread forwardPortThread = new Thread(threadGroup, this::forwardPort, "ForwardPort");
|
||||
forwardPortThread.start();
|
||||
}
|
||||
|
||||
private void forwardPort() {
|
||||
try {
|
||||
Matcher matcher = ADDRESS_PATTERN.matcher(address);
|
||||
if (!matcher.find()) {
|
||||
throw new MalformedURLException();
|
||||
}
|
||||
try (Socket forwardingSocket = new Socket();
|
||||
ServerSocket serverSocket = new ServerSocket()) {
|
||||
forwardingSocket.setSoTimeout(30000);
|
||||
forwardingSocket.connect(new InetSocketAddress(matcher.group(1), Lang.parseInt(matcher.group(2), 0)));
|
||||
|
||||
serverSocket.bind(null);
|
||||
|
||||
Thread broadcastMOTDThread = new Thread(threadGroup, () -> broadcastMOTD(serverSocket.getLocalPort()), "BroadcastMOTD");
|
||||
broadcastMOTDThread.start();
|
||||
|
||||
LOG.log(Level.INFO, "Listening " + serverSocket.getLocalSocketAddress());
|
||||
|
||||
while (running) {
|
||||
Socket forwardedSocket = serverSocket.accept();
|
||||
LOG.log(Level.INFO, "Accepting client");
|
||||
new Thread(threadGroup, () -> forwardTraffic(forwardingSocket, forwardedSocket), "Forward S->D").start();
|
||||
new Thread(threadGroup, () -> forwardTraffic(forwardedSocket, forwardingSocket), "Forward D->S").start();
|
||||
}
|
||||
}
|
||||
} catch (IOException | UnresolvedAddressException e) {
|
||||
LOG.log(Level.WARNING, "Error in forwarding port", e);
|
||||
} finally {
|
||||
close();
|
||||
onExit.fireEvent(new Event(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void forwardTraffic(Socket src, Socket dest) {
|
||||
try(InputStream is = src.getInputStream(); OutputStream os = dest.getOutputStream()) {
|
||||
byte[] buf = new byte[1024];
|
||||
while (true) {
|
||||
int len = is.read(buf, 0, buf.length);
|
||||
if (len < 0) break;
|
||||
LOG.log(Level.INFO, "Forwarding buffer " + len);
|
||||
os.write(buf, 0, len);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Disconnected", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastMOTD(int port) {
|
||||
DatagramSocket socket;
|
||||
InetAddress broadcastAddress;
|
||||
try {
|
||||
socket = new DatagramSocket();
|
||||
broadcastAddress = InetAddress.getByName("224.0.2.60");
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to create datagram socket", e);
|
||||
return;
|
||||
}
|
||||
|
||||
while (running) {
|
||||
try {
|
||||
byte[] data = String.format("[MOTD]%s[/MOTD][AD]%d[/AD]", i18n("multiplayer.session.name.motd"), port).getBytes(StandardCharsets.UTF_8);
|
||||
DatagramPacket packet = new DatagramPacket(data, 0, data.length, broadcastAddress, 4445);
|
||||
socket.send(packet);
|
||||
LOG.finest("Broadcast server 0.0.0.0:" + port);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send motd packet", e);
|
||||
}
|
||||
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
socket.close();
|
||||
}
|
||||
}
|
||||
@@ -1,431 +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.ui.multiplayer;
|
||||
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.gson.DateTypeAdapter;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.HttpRequest;
|
||||
import org.jackhuang.hmcl.util.io.NetworkUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Architecture;
|
||||
import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
||||
import org.jackhuang.hmcl.util.platform.ManagedProcess;
|
||||
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.util.Lang.*;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
|
||||
/**
|
||||
* Cato Management.
|
||||
*/
|
||||
public final class MultiplayerManager {
|
||||
static final String HIPER_VERSION = "1.2.2";
|
||||
private static final String HIPER_DOWNLOAD_URL = "https://gitcode.net/to/hiper/-/raw/master/";
|
||||
private static final String HIPER_PACKAGES_URL = HIPER_DOWNLOAD_URL + "packages.sha1";
|
||||
private static final String HIPER_POINTS_URL = "https://cert.mcer.cn/point.yml";
|
||||
private static final Path HIPER_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("hiper.yml");
|
||||
public static final Path HIPER_PATH = getHiperLocalDirectory().resolve(getHiperFileName());
|
||||
public static final int HIPER_AGREEMENT_VERSION = 3;
|
||||
private static final String REMOTE_ADDRESS = "127.0.0.1";
|
||||
private static final String LOCAL_ADDRESS = "0.0.0.0";
|
||||
|
||||
private static final Map<Architecture, String> archMap = mapOf(
|
||||
pair(Architecture.ARM32, "arm-7"),
|
||||
pair(Architecture.ARM64, "arm64"),
|
||||
pair(Architecture.X86, "386"),
|
||||
pair(Architecture.X86_64, "amd64"),
|
||||
pair(Architecture.LOONGARCH64, "loong64"),
|
||||
pair(Architecture.MIPS, "mips"),
|
||||
pair(Architecture.MIPS64, "mips64"),
|
||||
pair(Architecture.MIPS64EL, "mips64le"),
|
||||
pair(Architecture.PPC64LE, "ppc64le"),
|
||||
pair(Architecture.RISCV, "riscv64"),
|
||||
pair(Architecture.MIPSEL, "mipsle")
|
||||
);
|
||||
|
||||
private static final Map<OperatingSystem, String> osMap = mapOf(
|
||||
pair(OperatingSystem.LINUX, "linux"),
|
||||
pair(OperatingSystem.WINDOWS, "windows"),
|
||||
pair(OperatingSystem.OSX, "darwin")
|
||||
);
|
||||
|
||||
private static final String HIPER_TARGET_NAME = String.format("%s-%s",
|
||||
osMap.getOrDefault(OperatingSystem.CURRENT_OS, "windows"),
|
||||
archMap.getOrDefault(Architecture.CURRENT_ARCH, "amd64"));
|
||||
|
||||
|
||||
private static CompletableFuture<Map<String, String>> HASH;
|
||||
|
||||
private MultiplayerManager() {
|
||||
}
|
||||
|
||||
public static void clearConfiguration() {
|
||||
HIPER_CONFIG_PATH.toFile().delete();
|
||||
}
|
||||
|
||||
private static CompletableFuture<Map<String, String>> getPackagesHash() {
|
||||
FXUtils.checkFxUserThread();
|
||||
if (HASH == null) {
|
||||
HASH = CompletableFuture.supplyAsync(wrap(() -> {
|
||||
String hashList = HttpRequest.GET(HIPER_PACKAGES_URL).getString();
|
||||
Map<String, String> hashes = new HashMap<>();
|
||||
for (String line : hashList.split("\n")) {
|
||||
String[] items = line.trim().split(" {2}");
|
||||
if (items.length == 2 && items[0].length() == 40) {
|
||||
hashes.put(items[1], items[0]);
|
||||
} else {
|
||||
LOG.warning("Failed to parse Hiper packages.sha1 file, line: " + line);
|
||||
}
|
||||
}
|
||||
return hashes;
|
||||
}));
|
||||
}
|
||||
return HASH;
|
||||
}
|
||||
|
||||
public static Task<Void> downloadHiper() {
|
||||
return Task.fromCompletableFuture(getPackagesHash()).thenComposeAsync(packagesHash -> {
|
||||
|
||||
BiFunction<String, String, FileDownloadTask> getFileDownloadTask = (String remotePath, String localFileName) -> {
|
||||
String hash = packagesHash.get(remotePath);
|
||||
return new FileDownloadTask(
|
||||
NetworkUtils.toURL(String.format("%s%s", HIPER_DOWNLOAD_URL, remotePath)),
|
||||
getHiperLocalDirectory().resolve(localFileName).toFile(),
|
||||
hash == null ? null : new FileDownloadTask.IntegrityCheck("SHA-1", hash));
|
||||
};
|
||||
|
||||
List<Task<?>> tasks;
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
if (!packagesHash.containsKey(String.format("%s/hiper.exe", HIPER_TARGET_NAME))) {
|
||||
throw new HiperUnsupportedPlatformException();
|
||||
}
|
||||
tasks = Arrays.asList(
|
||||
getFileDownloadTask.apply(String.format("%s/hiper.exe", HIPER_TARGET_NAME), "hiper.exe"),
|
||||
getFileDownloadTask.apply(String.format("%s/wintun.dll", HIPER_TARGET_NAME), "wintun.dll")
|
||||
// getFileDownloadTask.apply("tap-windows-9.21.2.exe", "tap-windows-9.21.2.exe")
|
||||
);
|
||||
} else {
|
||||
if (!packagesHash.containsKey(String.format("%s/hiper", HIPER_TARGET_NAME))) {
|
||||
throw new HiperUnsupportedPlatformException();
|
||||
}
|
||||
tasks = Collections.singletonList(getFileDownloadTask.apply(String.format("%s/hiper", HIPER_TARGET_NAME), "hiper"));
|
||||
}
|
||||
return Task.allOf(tasks).thenRunAsync(() -> {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
|
||||
Set<PosixFilePermission> perm = Files.getPosixFilePermissions(HIPER_PATH);
|
||||
perm.add(PosixFilePermission.OWNER_EXECUTE);
|
||||
Files.setPosixFilePermissions(HIPER_PATH, perm);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static void verifyChecksumAndDeleteIfNotMatched(Path file, @Nullable String expectedChecksum) throws IOException {
|
||||
try {
|
||||
if (expectedChecksum != null) {
|
||||
ChecksumMismatchException.verifyChecksum(file, "SHA-1", expectedChecksum);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Files.deleteIfExists(file);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<HiperSession> startHiper(String token) {
|
||||
return getPackagesHash().thenApplyAsync(wrap(packagesHash -> {
|
||||
if (!Files.isRegularFile(HIPER_PATH)) {
|
||||
throw new HiperNotExistsException(HIPER_PATH);
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
verifyChecksumAndDeleteIfNotMatched(getHiperLocalDirectory().resolve("hiper.exe"), packagesHash.get(String.format("%s/hiper.exe", HIPER_TARGET_NAME)));
|
||||
verifyChecksumAndDeleteIfNotMatched(getHiperLocalDirectory().resolve("wintun.dll"), packagesHash.get(String.format("%s/wintun.dll", HIPER_TARGET_NAME)));
|
||||
// verifyChecksumAndDeleteIfNotMatched(getHiperLocalDirectory().resolve("tap-windows-9.21.2.exe"), packagesHash.get("tap-windows-9.21.2.exe"));
|
||||
} else {
|
||||
verifyChecksumAndDeleteIfNotMatched(getHiperLocalDirectory().resolve("hiper"), packagesHash.get(String.format("%s/hiper", HIPER_TARGET_NAME)));
|
||||
}
|
||||
|
||||
// 下载 HiPer 配置文件
|
||||
String certFileContent;
|
||||
try {
|
||||
certFileContent = HttpRequest.GET(String.format("https://cert.mcer.cn/%s.yml", token)).getString();
|
||||
if (!certFileContent.equals("")) {
|
||||
certFileContent += "\nlogging:\n format: json\n file_path: ./hiper.log";
|
||||
FileUtils.writeText(HIPER_CONFIG_PATH, certFileContent);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "configuration file cloud cache index code has been not available , try to use the local configuration file", e);
|
||||
}
|
||||
|
||||
String[] commands = new String[]{HIPER_PATH.toString(), "-config", HIPER_CONFIG_PATH.toString()};
|
||||
Process process = new ProcessBuilder()
|
||||
.command(commands)
|
||||
.start();
|
||||
|
||||
return new HiperSession(process, Arrays.asList(commands));
|
||||
}));
|
||||
}
|
||||
|
||||
public static String getHiperFileName() {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
return "hiper.exe";
|
||||
} else {
|
||||
return "hiper";
|
||||
}
|
||||
}
|
||||
|
||||
public static Path getHiperLocalDirectory() {
|
||||
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve("hiper").resolve("hiper").resolve(HIPER_VERSION);
|
||||
}
|
||||
|
||||
public static class HiperSession extends ManagedProcess {
|
||||
private final EventManager<HiperExitEvent> onExit = new EventManager<>();
|
||||
private final EventManager<HiperIPEvent> onIPAllocated = new EventManager<>();
|
||||
private final EventManager<HiperShowValidUntilEvent> onValidUntil = new EventManager<>();
|
||||
private final BufferedWriter writer;
|
||||
private int error = 0;
|
||||
|
||||
HiperSession(Process process, List<String> commands) {
|
||||
super(process, commands);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
|
||||
|
||||
LOG.info("Started hiper with command: " + new CommandBuilder().addAll(commands));
|
||||
|
||||
addRelatedThread(Lang.thread(this::waitFor, "HiperExitWaiter", true));
|
||||
pumpInputStream(this::onLog);
|
||||
pumpErrorStream(this::onLog);
|
||||
|
||||
writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private void onLog(String log) {
|
||||
LOG.info("[Hiper] " + log);
|
||||
|
||||
if (log.contains("failed to load config")) {
|
||||
error = HiperExitEvent.INVALID_CONFIGURATION;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<?, ?> logJson = JsonUtils.fromNonNullJson(log, Map.class);
|
||||
String msg = "";
|
||||
if (logJson.containsKey("msg")) {
|
||||
msg = tryCast(logJson.get("msg"), String.class).orElse("");
|
||||
if (msg.contains("Failed to get a tun/tap device")) {
|
||||
error = HiperExitEvent.FAILED_GET_DEVICE;
|
||||
}
|
||||
if (msg.contains("Failed to load certificate from config")) {
|
||||
error = HiperExitEvent.FAILED_LOAD_CONFIG;
|
||||
}
|
||||
if (msg.contains("Validity of client certificate")) {
|
||||
Optional<String> validUntil = tryCast(logJson.get("valid"), String.class);
|
||||
if (validUntil.isPresent()) {
|
||||
try {
|
||||
Date date = DateTypeAdapter.deserializeToDate(validUntil.get());
|
||||
onValidUntil.fireEvent(new HiperShowValidUntilEvent(this, date));
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse certification expire time string: " + validUntil.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (logJson.containsKey("network")) {
|
||||
Map<?, ?> network = tryCast(logJson.get("network"), Map.class).orElse(Collections.emptyMap());
|
||||
if (network.containsKey("IP") && msg.contains("Main HostMap created")) {
|
||||
Optional<String> ip = tryCast(network.get("IP"), String.class);
|
||||
ip.ifPresent(s -> onIPAllocated.fireEvent(new HiperIPEvent(this, s)));
|
||||
}
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
LOG.log(Level.WARNING, "Failed to parse hiper log: " + log, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitFor() {
|
||||
try {
|
||||
int exitCode = getProcess().waitFor();
|
||||
LOG.info("Hiper exited with exitcode " + exitCode);
|
||||
if (error != 0) {
|
||||
onExit.fireEvent(new HiperExitEvent(this, error));
|
||||
} else {
|
||||
onExit.fireEvent(new HiperExitEvent(this, exitCode));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
onExit.fireEvent(new HiperExitEvent(this, HiperExitEvent.INTERRUPTED));
|
||||
} finally {
|
||||
try {
|
||||
if (writer != null)
|
||||
writer.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to close Hiper stdin writer", e);
|
||||
}
|
||||
}
|
||||
destroyRelatedThreads();
|
||||
}
|
||||
|
||||
public EventManager<HiperExitEvent> onExit() {
|
||||
return onExit;
|
||||
}
|
||||
|
||||
public EventManager<HiperIPEvent> onIPAllocated() {
|
||||
return onIPAllocated;
|
||||
}
|
||||
|
||||
public EventManager<HiperShowValidUntilEvent> onValidUntil() {
|
||||
return onValidUntil;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class HiperExitEvent extends Event {
|
||||
private final int exitCode;
|
||||
|
||||
public HiperExitEvent(Object source, int exitCode) {
|
||||
super(source);
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public static final int INTERRUPTED = -1;
|
||||
public static final int INVALID_CONFIGURATION = -2;
|
||||
public static final int CERTIFICATE_EXPIRED = -3;
|
||||
public static final int FAILED_GET_DEVICE = -4;
|
||||
public static final int FAILED_LOAD_CONFIG = -5;
|
||||
}
|
||||
|
||||
public static class HiperIPEvent extends Event {
|
||||
private final String ip;
|
||||
|
||||
public HiperIPEvent(Object source, String ip) {
|
||||
super(source);
|
||||
this.ip = ip;
|
||||
}
|
||||
|
||||
public String getIP() {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HiperShowValidUntilEvent extends Event {
|
||||
private final Date validAt;
|
||||
|
||||
public HiperShowValidUntilEvent(Object source, Date validAt) {
|
||||
super(source);
|
||||
this.validAt = validAt;
|
||||
}
|
||||
|
||||
public Date getValidUntil() {
|
||||
return validAt;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HiperExitException extends RuntimeException {
|
||||
private final int exitCode;
|
||||
private final boolean ready;
|
||||
|
||||
public HiperExitException(int exitCode, boolean ready) {
|
||||
this.exitCode = exitCode;
|
||||
this.ready = ready;
|
||||
}
|
||||
|
||||
public int getExitCode() {
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HiperExitTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class HiperSessionExpiredException extends HiperInvalidConfigurationException {
|
||||
}
|
||||
|
||||
public static class HiperInvalidConfigurationException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class JoinRequestTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class PeerConnectionTimeoutException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class ConnectionErrorException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class KickedException extends RuntimeException {
|
||||
private final String reason;
|
||||
|
||||
public KickedException(String reason) {
|
||||
this.reason = reason;
|
||||
}
|
||||
|
||||
public String getReason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HiperNotExistsException extends RuntimeException {
|
||||
private final Path file;
|
||||
|
||||
public HiperNotExistsException(Path file) {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
public Path getFile() {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
public static class HiperInvalidTokenException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class HiperUnsupportedPlatformException extends RuntimeException {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,307 +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.ui.multiplayer;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXDialogLayout;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
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.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.function.Consumer;
|
||||
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.util.Lang.resolveException;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorPage, PageAware {
|
||||
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer")));
|
||||
|
||||
private final ReadOnlyObjectWrapper<MultiplayerManager.HiperSession> session = new ReadOnlyObjectWrapper<>();
|
||||
private final IntegerProperty port = new SimpleIntegerProperty();
|
||||
private final StringProperty address = new SimpleStringProperty();
|
||||
private final ReadOnlyObjectWrapper<Date> expireTime = new ReadOnlyObjectWrapper<>();
|
||||
|
||||
private Consumer<MultiplayerManager.HiperExitEvent> onExit;
|
||||
private Consumer<MultiplayerManager.HiperIPEvent> onIPAllocated;
|
||||
private Consumer<MultiplayerManager.HiperShowValidUntilEvent> onValidUntil;
|
||||
|
||||
private final ReadOnlyObjectWrapper<LocalServerBroadcaster> broadcaster = new ReadOnlyObjectWrapper<>();
|
||||
private Consumer<Event> onBroadcasterExit = null;
|
||||
|
||||
public MultiplayerPage() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageShown() {
|
||||
checkAgreement(this::downloadHiPerIfNecessary);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
return new MultiplayerPageSkin(this);
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port.get();
|
||||
}
|
||||
|
||||
public IntegerProperty portProperty() {
|
||||
return port;
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
this.port.set(port);
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
return address.get();
|
||||
}
|
||||
|
||||
public StringProperty addressProperty() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(String address) {
|
||||
this.address.set(address);
|
||||
}
|
||||
|
||||
public LocalServerBroadcaster getBroadcaster() {
|
||||
return broadcaster.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectWrapper<LocalServerBroadcaster> broadcasterProperty() {
|
||||
return broadcaster;
|
||||
}
|
||||
|
||||
public void setBroadcaster(LocalServerBroadcaster broadcaster) {
|
||||
this.broadcaster.set(broadcaster);
|
||||
}
|
||||
|
||||
public Date getExpireTime() {
|
||||
return expireTime.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectWrapper<Date> expireTimeProperty() {
|
||||
return expireTime;
|
||||
}
|
||||
|
||||
public void setExpireTime(Date expireTime) {
|
||||
this.expireTime.set(expireTime);
|
||||
}
|
||||
|
||||
public MultiplayerManager.HiperSession getSession() {
|
||||
return session.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<MultiplayerManager.HiperSession> sessionProperty() {
|
||||
return session.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
private void checkAgreement(Runnable runnable) {
|
||||
if (globalConfig().getMultiplayerAgreementVersion() < MultiplayerManager.HIPER_AGREEMENT_VERSION) {
|
||||
JFXDialogLayout agreementPane = new JFXDialogLayout();
|
||||
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 -> HMCLService.openRedirectLink("multiplayer-agreement"));
|
||||
JFXButton yesButton = new JFXButton(i18n("launcher.agreement.accept"));
|
||||
yesButton.getStyleClass().add("dialog-accept");
|
||||
yesButton.setOnAction(e -> {
|
||||
globalConfig().setMultiplayerAgreementVersion(MultiplayerManager.HIPER_AGREEMENT_VERSION);
|
||||
runnable.run();
|
||||
agreementPane.fireEvent(new DialogCloseEvent());
|
||||
});
|
||||
JFXButton noButton = new JFXButton(i18n("launcher.agreement.decline"));
|
||||
noButton.getStyleClass().add("dialog-cancel");
|
||||
noButton.setOnAction(e -> {
|
||||
agreementPane.fireEvent(new DialogCloseEvent());
|
||||
fireEvent(new PageCloseEvent());
|
||||
});
|
||||
agreementPane.setActions(agreementLink, yesButton, noButton);
|
||||
Controllers.dialog(agreementPane);
|
||||
} else {
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void downloadHiPerIfNecessary() {
|
||||
if (!MultiplayerManager.HIPER_PATH.toFile().exists()) {
|
||||
setDisabled(true);
|
||||
Controllers.taskDialog(MultiplayerManager.downloadHiper()
|
||||
.whenComplete(Schedulers.javafx(), exception -> {
|
||||
setDisabled(false);
|
||||
if (exception != null) {
|
||||
if (exception instanceof CancellationException) {
|
||||
Controllers.showToast(i18n("message.cancelled"));
|
||||
} else if (exception instanceof MultiplayerManager.HiperUnsupportedPlatformException) {
|
||||
Controllers.dialog(i18n("multiplayer.download.unsupported"), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
fireEvent(new PageCloseEvent());
|
||||
} else {
|
||||
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
|
||||
fireEvent(new PageCloseEvent());
|
||||
}
|
||||
} else {
|
||||
Controllers.showToast(i18n("multiplayer.download.success"));
|
||||
}
|
||||
}), i18n("multiplayer.download"), TaskCancellationAction.NORMAL);
|
||||
} else {
|
||||
setDisabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
private String localizeErrorMessage(Throwable t) {
|
||||
Throwable e = resolveException(t);
|
||||
if (e instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
return i18n("message.cancelled");
|
||||
} else if (e instanceof MultiplayerManager.HiperInvalidConfigurationException) {
|
||||
LOG.info("Hiper invalid configuration");
|
||||
return i18n("multiplayer.token.malformed");
|
||||
} else if (e instanceof MultiplayerManager.HiperNotExistsException) {
|
||||
LOG.log(Level.WARNING, "Hiper not found " + ((MultiplayerManager.HiperNotExistsException) e).getFile(), e);
|
||||
return i18n("multiplayer.error.file_not_found");
|
||||
} else if (e instanceof MultiplayerManager.HiperExitException) {
|
||||
LOG.info("HiPer exited accidentally");
|
||||
int exitCode = ((MultiplayerManager.HiperExitException) e).getExitCode();
|
||||
return i18n("multiplayer.exit", exitCode);
|
||||
} else if (e instanceof MultiplayerManager.HiperInvalidTokenException) {
|
||||
LOG.info("invalid token");
|
||||
return i18n("multiplayer.token.invalid");
|
||||
} else if (e instanceof ChecksumMismatchException) {
|
||||
return i18n("exception.artifact_malformed");
|
||||
} else {
|
||||
return e.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public void start() {
|
||||
MultiplayerManager.startHiper(globalConfig().getMultiplayerToken())
|
||||
.thenAcceptAsync(session -> {
|
||||
this.session.set(session);
|
||||
onExit = session.onExit().registerWeak(this::onExit);
|
||||
onIPAllocated = session.onIPAllocated().registerWeak(this::onIPAllocated);
|
||||
onValidUntil = session.onValidUntil().registerWeak(this::onValidUntil);
|
||||
}, Schedulers.javafx())
|
||||
.exceptionally(throwable -> {
|
||||
runInFX(() -> Controllers.dialog(localizeErrorMessage(throwable), null, MessageDialogPane.MessageType.ERROR));
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (getSession() != null) {
|
||||
getSession().stop();
|
||||
}
|
||||
if (getBroadcaster() != null) {
|
||||
getBroadcaster().close();
|
||||
}
|
||||
clearSession();
|
||||
}
|
||||
|
||||
public void broadcast(String url) {
|
||||
LocalServerBroadcaster broadcaster = new LocalServerBroadcaster(url);
|
||||
this.onBroadcasterExit = broadcaster.onExit().registerWeak(this::onBroadcasterExit);
|
||||
broadcaster.start();
|
||||
this.broadcaster.set(broadcaster);
|
||||
}
|
||||
|
||||
public void stopBroadcasting() {
|
||||
if (getBroadcaster() != null) {
|
||||
getBroadcaster().close();
|
||||
setBroadcaster(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onBroadcasterExit(Event event) {
|
||||
runInFX(() -> {
|
||||
if (this.broadcaster.get() == event.getSource()) {
|
||||
this.broadcaster.set(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void clearSession() {
|
||||
this.session.set(null);
|
||||
this.onExit = null;
|
||||
this.onIPAllocated = null;
|
||||
this.onValidUntil = null;
|
||||
this.broadcaster.set(null);
|
||||
this.onBroadcasterExit = null;
|
||||
}
|
||||
|
||||
private void onIPAllocated(MultiplayerManager.HiperIPEvent event) {
|
||||
runInFX(() -> this.address.set(event.getIP()));
|
||||
}
|
||||
|
||||
private void onValidUntil(MultiplayerManager.HiperShowValidUntilEvent event) {
|
||||
runInFX(() -> this.expireTime.set(event.getValidUntil()));
|
||||
}
|
||||
|
||||
private void onExit(MultiplayerManager.HiperExitEvent event) {
|
||||
runInFX(() -> {
|
||||
switch (event.getExitCode()) {
|
||||
case 0:
|
||||
break;
|
||||
case MultiplayerManager.HiperExitEvent.CERTIFICATE_EXPIRED:
|
||||
MultiplayerManager.clearConfiguration();
|
||||
Controllers.dialog(i18n("multiplayer.token.expired"));
|
||||
break;
|
||||
case MultiplayerManager.HiperExitEvent.INVALID_CONFIGURATION:
|
||||
MultiplayerManager.clearConfiguration();
|
||||
Controllers.dialog(i18n("multiplayer.token.malformed"));
|
||||
break;
|
||||
case MultiplayerManager.HiperExitEvent.INTERRUPTED:
|
||||
// do nothing
|
||||
break;
|
||||
case MultiplayerManager.HiperExitEvent.FAILED_GET_DEVICE:
|
||||
Controllers.dialog(i18n("multiplayer.error.failed_get_device"));
|
||||
break;
|
||||
case MultiplayerManager.HiperExitEvent.FAILED_LOAD_CONFIG:
|
||||
Controllers.dialog(i18n("multiplayer.error.failed_load_config"));
|
||||
break;
|
||||
default:
|
||||
Controllers.dialog(i18n("multiplayer.exit", event.getExitCode()));
|
||||
break;
|
||||
}
|
||||
|
||||
// clearSession();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
return state;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,311 +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.ui.multiplayer;
|
||||
|
||||
import com.jfoenix.controls.JFXButton;
|
||||
import com.jfoenix.controls.JFXPasswordField;
|
||||
import com.jfoenix.controls.JFXTextField;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
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.Lang;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
|
||||
public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimatedPageSkin<MultiplayerPage> {
|
||||
|
||||
private ObservableList<Node> clients;
|
||||
|
||||
/**
|
||||
* Constructor for all SkinBase instances.
|
||||
*
|
||||
* @param control The control for which this Skin should attach to.
|
||||
*/
|
||||
protected MultiplayerPageSkin(MultiplayerPage control) {
|
||||
super(control);
|
||||
|
||||
{
|
||||
AdvancedListBox sideBar = new AdvancedListBox()
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("version.launch"));
|
||||
item.setLeftGraphic(wrap(SVG::rocketLaunchOutline));
|
||||
item.setOnAction(e -> {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep);
|
||||
});
|
||||
})
|
||||
.startCategory(i18n("help"))
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("help"));
|
||||
item.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer"));
|
||||
})
|
||||
.addNavigationDrawerItem(report -> {
|
||||
report.setTitle(i18n("feedback"));
|
||||
report.setLeftGraphic(wrap(SVG::messageAlertOutline));
|
||||
report.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-feedback"));
|
||||
});
|
||||
FXUtils.setLimitWidth(sideBar, 200);
|
||||
setLeft(sideBar);
|
||||
}
|
||||
|
||||
{
|
||||
VBox content = new VBox(16);
|
||||
content.setPadding(new Insets(10));
|
||||
content.setFillWidth(true);
|
||||
ScrollPane scrollPane = new ScrollPane(content);
|
||||
scrollPane.setFitToWidth(true);
|
||||
setCenter(scrollPane);
|
||||
|
||||
VBox mainPane = new VBox(16);
|
||||
{
|
||||
ComponentList offPane = new ComponentList();
|
||||
{
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
hintPane.setText(i18n("multiplayer.off.hint"));
|
||||
|
||||
BorderPane tokenPane = new BorderPane();
|
||||
{
|
||||
Label tokenTitle = new Label(i18n("multiplayer.token"));
|
||||
BorderPane.setAlignment(tokenTitle, Pos.CENTER_LEFT);
|
||||
tokenPane.setLeft(tokenTitle);
|
||||
// Token acts like password, we hide it here preventing users from accidentally leaking their token when taking screenshots.
|
||||
JFXPasswordField tokenField = new JFXPasswordField();
|
||||
BorderPane.setAlignment(tokenField, Pos.CENTER_LEFT);
|
||||
BorderPane.setMargin(tokenField, new Insets(0, 8, 0, 8));
|
||||
tokenPane.setCenter(tokenField);
|
||||
tokenField.textProperty().bindBidirectional(globalConfig().multiplayerTokenProperty());
|
||||
tokenField.setPromptText(i18n("multiplayer.token.prompt"));
|
||||
|
||||
JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.token.apply"));
|
||||
BorderPane.setAlignment(applyLink, Pos.CENTER_RIGHT);
|
||||
applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token"));
|
||||
tokenPane.setRight(applyLink);
|
||||
}
|
||||
|
||||
HBox startPane = new HBox();
|
||||
{
|
||||
JFXButton startButton = new JFXButton(i18n("multiplayer.off.start"));
|
||||
startButton.getStyleClass().add("jfx-button-raised");
|
||||
startButton.setButtonType(JFXButton.ButtonType.RAISED);
|
||||
startButton.setOnMouseClicked(e -> control.start());
|
||||
|
||||
startPane.getChildren().setAll(startButton);
|
||||
startPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
}
|
||||
|
||||
offPane.getContent().setAll(hintPane, tokenPane, startPane);
|
||||
}
|
||||
|
||||
ComponentList onPane = new ComponentList();
|
||||
{
|
||||
BorderPane expirationPane = new BorderPane();
|
||||
expirationPane.setLeft(new Label(i18n("multiplayer.session.expiration")));
|
||||
Label expirationLabel = new Label();
|
||||
expirationLabel.textProperty().bind(Bindings.createStringBinding(() ->
|
||||
control.getExpireTime() == null ? "" : Locales.SIMPLE_DATE_FORMAT.get().format(control.getExpireTime()),
|
||||
control.expireTimeProperty()));
|
||||
expirationPane.setCenter(expirationLabel);
|
||||
|
||||
GridPane masterPane = new GridPane();
|
||||
masterPane.setVgap(8);
|
||||
masterPane.setHgap(16);
|
||||
ColumnConstraints titleColumn = new ColumnConstraints();
|
||||
ColumnConstraints valueColumn = new ColumnConstraints();
|
||||
ColumnConstraints rightColumn = new ColumnConstraints();
|
||||
masterPane.getColumnConstraints().setAll(titleColumn, valueColumn, rightColumn);
|
||||
valueColumn.setFillWidth(true);
|
||||
valueColumn.setHgrow(Priority.ALWAYS);
|
||||
{
|
||||
BorderPane titlePane = new BorderPane();
|
||||
GridPane.setColumnSpan(titlePane, 3);
|
||||
Label title = new Label(i18n("multiplayer.master"));
|
||||
titlePane.setLeft(title);
|
||||
|
||||
JFXHyperlink tutorial = new JFXHyperlink(i18n("multiplayer.master.video_tutorial"));
|
||||
titlePane.setRight(tutorial);
|
||||
tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-master"));
|
||||
masterPane.addRow(0, titlePane);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
GridPane.setColumnSpan(hintPane, 3);
|
||||
hintPane.setText(i18n("multiplayer.master.hint"));
|
||||
masterPane.addRow(1, hintPane);
|
||||
|
||||
Label portTitle = new Label(i18n("multiplayer.master.port"));
|
||||
BorderPane.setAlignment(portTitle, Pos.CENTER_LEFT);
|
||||
|
||||
JFXTextField portTextField = new JFXTextField();
|
||||
GridPane.setColumnSpan(portTextField, 2);
|
||||
FXUtils.setValidateWhileTextChanged(portTextField, true);
|
||||
portTextField.getValidators().add(new Validator(i18n("multiplayer.master.port.validate"), (text) -> {
|
||||
Integer value = Lang.toIntOrNull(text);
|
||||
return value != null && 0 <= value && value <= 65535;
|
||||
}));
|
||||
portTextField.textProperty().bindBidirectional(control.portProperty(), new StringConverter<Number>() {
|
||||
@Override
|
||||
public String toString(Number object) {
|
||||
return Integer.toString(object.intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Number fromString(String string) {
|
||||
return Lang.parseInt(string, 0);
|
||||
}
|
||||
});
|
||||
masterPane.addRow(2, portTitle, portTextField);
|
||||
|
||||
Label serverAddressTitle = new Label(i18n("multiplayer.master.server_address"));
|
||||
BorderPane.setAlignment(serverAddressTitle, Pos.CENTER_LEFT);
|
||||
Label serverAddressLabel = new Label();
|
||||
BorderPane.setAlignment(serverAddressLabel, Pos.CENTER_LEFT);
|
||||
serverAddressLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
return (control.getAddress() == null ? "" : control.getAddress()) + ":" + control.getPort();
|
||||
}, control.addressProperty(), control.portProperty()));
|
||||
JFXButton copyButton = new JFXButton(i18n("multiplayer.master.server_address.copy"));
|
||||
copyButton.setOnAction(e -> FXUtils.copyText(serverAddressLabel.getText()));
|
||||
masterPane.addRow(3, serverAddressTitle, serverAddressLabel, copyButton);
|
||||
}
|
||||
|
||||
VBox slavePane = new VBox(8);
|
||||
{
|
||||
BorderPane titlePane = new BorderPane();
|
||||
Label title = new Label(i18n("multiplayer.slave"));
|
||||
titlePane.setLeft(title);
|
||||
|
||||
JFXHyperlink tutorial = new JFXHyperlink(i18n("multiplayer.slave.video_tutorial"));
|
||||
tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-slave"));
|
||||
titlePane.setRight(tutorial);
|
||||
|
||||
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
|
||||
GridPane.setColumnSpan(hintPane, 3);
|
||||
hintPane.setText(i18n("multiplayer.slave.hint"));
|
||||
slavePane.getChildren().add(hintPane);
|
||||
|
||||
GridPane notBroadcastingPane = new GridPane();
|
||||
{
|
||||
notBroadcastingPane.setVgap(8);
|
||||
notBroadcastingPane.setHgap(16);
|
||||
notBroadcastingPane.getColumnConstraints().setAll(titleColumn, valueColumn, rightColumn);
|
||||
|
||||
Label addressTitle = new Label(i18n("multiplayer.slave.server_address"));
|
||||
|
||||
JFXTextField addressField = new JFXTextField();
|
||||
FXUtils.setValidateWhileTextChanged(addressField, true);
|
||||
addressField.getValidators().add(new ServerAddressValidator());
|
||||
|
||||
JFXButton startButton = new JFXButton(i18n("multiplayer.slave.server_address.start"));
|
||||
startButton.setOnAction(e -> control.broadcast(addressField.getText()));
|
||||
notBroadcastingPane.addRow(0, addressTitle, addressField, startButton);
|
||||
}
|
||||
|
||||
GridPane broadcastingPane = new GridPane();
|
||||
{
|
||||
broadcastingPane.setVgap(8);
|
||||
broadcastingPane.setHgap(16);
|
||||
broadcastingPane.getColumnConstraints().setAll(titleColumn, valueColumn, rightColumn);
|
||||
|
||||
Label addressTitle = new Label(i18n("multiplayer.slave.server_address"));
|
||||
Label addressLabel = new Label();
|
||||
addressLabel.textProperty().bind(Bindings.createStringBinding(() ->
|
||||
control.getBroadcaster() != null ? control.getBroadcaster().getAddress() : "",
|
||||
control.broadcasterProperty()));
|
||||
|
||||
JFXButton stopButton = new JFXButton(i18n("multiplayer.slave.server_address.stop"));
|
||||
stopButton.setOnAction(e -> control.stopBroadcasting());
|
||||
broadcastingPane.addRow(0, addressTitle, addressLabel, stopButton);
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.broadcasterProperty(), broadcaster -> {
|
||||
if (broadcaster == null) {
|
||||
slavePane.getChildren().setAll(titlePane, hintPane, notBroadcastingPane);
|
||||
} else {
|
||||
slavePane.getChildren().setAll(titlePane, hintPane, broadcastingPane);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(control.expireTimeProperty(), t -> {
|
||||
if (t == null) {
|
||||
onPane.getContent().setAll(masterPane, slavePane);
|
||||
} else {
|
||||
onPane.getContent().setAll(expirationPane, masterPane, slavePane);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
FXUtils.onChangeAndOperate(getSkinnable().sessionProperty(), session -> {
|
||||
if (session == null) {
|
||||
mainPane.getChildren().setAll(ComponentList.createComponentListTitle(i18n("multiplayer.off")),
|
||||
offPane);
|
||||
} else {
|
||||
mainPane.getChildren().setAll(ComponentList.createComponentListTitle(i18n("multiplayer.on")),
|
||||
onPane);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ComponentList thanksPane = new ComponentList();
|
||||
{
|
||||
HBox pane = new HBox();
|
||||
pane.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
JFXHyperlink aboutLink = new JFXHyperlink(i18n("about"));
|
||||
aboutLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-about"));
|
||||
|
||||
HBox placeholder = new HBox();
|
||||
HBox.setHgrow(placeholder, Priority.ALWAYS);
|
||||
|
||||
pane.getChildren().setAll(
|
||||
new Label("Based on HiPer"),
|
||||
aboutLink,
|
||||
placeholder,
|
||||
FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction));
|
||||
|
||||
thanksPane.getContent().addAll(pane);
|
||||
}
|
||||
|
||||
content.getChildren().setAll(
|
||||
mainPane,
|
||||
ComponentList.createComponentListTitle(i18n("about")),
|
||||
thanksPane
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -47,6 +47,8 @@ public class DatapackListPage extends ListPageBase<DatapackListPageSkin.Datapack
|
||||
private final Path worldDir;
|
||||
private final Datapack datapack;
|
||||
|
||||
// Strongly referencing items, preventing GC collection
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final ObservableList<DatapackListPageSkin.DatapackInfoObject> items;
|
||||
|
||||
public DatapackListPage(String worldName, Path worldDir) {
|
||||
|
||||
@@ -32,7 +32,10 @@ public class ModDownloadListPage extends DownloadListPage {
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
|
||||
downloadSource.set("mods.curseforge");
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
downloadSource.set("mods.curseforge");
|
||||
else
|
||||
downloadSource.set("mods.modrinth");
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteModRepository {
|
||||
|
||||
@@ -31,6 +31,7 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
@@ -84,12 +85,9 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
toolbarNormal.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.refresh"), SVG::refresh, skinnable::refresh),
|
||||
createToolbarButton2(i18n("mods.add"), SVG::plus, skinnable::add),
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, () ->
|
||||
skinnable.openModFolder()),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, () ->
|
||||
skinnable.checkUpdates()),
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, () ->
|
||||
skinnable.download()));
|
||||
createToolbarButton2(i18n("folder.mod"), SVG::folderOpen, skinnable::openModFolder),
|
||||
createToolbarButton2(i18n("mods.check_updates"), SVG::update, skinnable::checkUpdates),
|
||||
createToolbarButton2(i18n("download"), SVG::downloadOutline, skinnable::download));
|
||||
HBox toolbarSelecting = new HBox();
|
||||
toolbarSelecting.getChildren().setAll(
|
||||
createToolbarButton2(i18n("button.remove"), SVG::delete, () -> {
|
||||
@@ -121,7 +119,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
center.getStyleClass().add("large-spinner-pane");
|
||||
center.loadingProperty().bind(skinnable.loadingProperty());
|
||||
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView));
|
||||
MutableObject<Object> lastCell = new MutableObject<>();
|
||||
listView.setCellFactory(x -> new ModInfoListCell(listView, lastCell));
|
||||
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
Bindings.bindContent(listView.getItems(), skinnable.getItems());
|
||||
|
||||
@@ -276,10 +275,10 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
private static Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
private static final Lazy<PopupMenu> menu = new Lazy<>(PopupMenu::new);
|
||||
private static final Lazy<JFXPopup> popup = new Lazy<>(() -> new JFXPopup(menu.get()));
|
||||
|
||||
class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
final class ModInfoListCell extends MDListCell<ModInfoObject> {
|
||||
JFXCheckBox checkBox = new JFXCheckBox();
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
JFXButton restoreButton = new JFXButton();
|
||||
@@ -287,8 +286,8 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
JFXButton revealButton = new JFXButton();
|
||||
BooleanProperty booleanProperty;
|
||||
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView) {
|
||||
super(listView);
|
||||
ModInfoListCell(JFXListView<ModInfoObject> listView, MutableObject<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
HBox container = new HBox(8);
|
||||
container.setPickOnBounds(false);
|
||||
@@ -332,6 +331,7 @@ class ModListPageSkin extends SkinBase<ModListPage> {
|
||||
restoreButton.setOnMouseClicked(e -> {
|
||||
menu.get().getContent().setAll(dataItem.getModInfo().getMod().getOldFiles().stream()
|
||||
.map(localModFile -> new IconedMenuItem(null, localModFile.getVersion(), () -> {
|
||||
popup.get().hide();
|
||||
getSkinnable().rollback(dataItem.getModInfo(), localModFile);
|
||||
}))
|
||||
.collect(Collectors.toList())
|
||||
|
||||
@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -123,7 +122,7 @@ public enum ModTranslations {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName), StandardCharsets.UTF_8);
|
||||
String modData = IOUtils.readFullyAsString(ModTranslations.class.getResourceAsStream(resourceName));
|
||||
mods = Arrays.stream(modData.split("\n")).filter(line -> !line.startsWith("#")).map(Mod::new).collect(Collectors.toList());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import org.apache.commons.lang3.mutable.MutableObject;
|
||||
import org.jackhuang.hmcl.mod.LocalModFile;
|
||||
import org.jackhuang.hmcl.mod.ModManager;
|
||||
import org.jackhuang.hmcl.mod.RemoteMod;
|
||||
@@ -158,8 +159,8 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
public static class ModUpdateCell extends MDListCell<LocalModFile.ModUpdate> {
|
||||
TwoLineListItem content = new TwoLineListItem();
|
||||
|
||||
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView) {
|
||||
super(listView);
|
||||
public ModUpdateCell(JFXListView<LocalModFile.ModUpdate> listView, MutableObject<Object> lastCell) {
|
||||
super(listView, lastCell);
|
||||
|
||||
getContainer().getChildren().setAll(content);
|
||||
}
|
||||
@@ -276,9 +277,7 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
dependents = mods.stream()
|
||||
.map(mod -> {
|
||||
return Task
|
||||
.runAsync(Schedulers.javafx(), () -> {
|
||||
mod.getKey().setOld(true);
|
||||
})
|
||||
.runAsync(Schedulers.javafx(), () -> mod.getKey().setOld(true))
|
||||
.thenComposeAsync(() -> {
|
||||
FileDownloadTask task = new FileDownloadTask(
|
||||
new URL(mod.getValue().getFile().getUrl()),
|
||||
@@ -325,6 +324,8 @@ public class ModUpdatesPage extends BorderPane implements DecoratorPage {
|
||||
|
||||
@Override
|
||||
public void execute() throws Exception {
|
||||
if (!isDependentsSucceeded())
|
||||
throw getException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,10 @@ public class ModpackDownloadListPage extends DownloadListPage {
|
||||
|
||||
supportChinese.set(true);
|
||||
downloadSources.get().setAll("mods.curseforge", "mods.modrinth");
|
||||
downloadSource.set("mods.curseforge");
|
||||
if (CurseForgeRemoteModRepository.isAvailable())
|
||||
downloadSource.set("mods.curseforge");
|
||||
else
|
||||
downloadSource.set("mods.modrinth");
|
||||
}
|
||||
|
||||
private class Repository extends LocalizedRemoteModRepository {
|
||||
|
||||
@@ -66,7 +66,8 @@ public class VersionIconDialog extends DialogPane {
|
||||
createIcon(VersionIconType.CRAFT_TABLE),
|
||||
createIcon(VersionIconType.FABRIC),
|
||||
createIcon(VersionIconType.FORGE),
|
||||
createIcon(VersionIconType.FURNACE)
|
||||
createIcon(VersionIconType.FURNACE),
|
||||
createIcon(VersionIconType.QUILT)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.Pair;
|
||||
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;
|
||||
@@ -259,7 +258,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
JFXTextField txtMaxMemory = new JFXTextField();
|
||||
FXUtils.setLimitWidth(txtMaxMemory, 60);
|
||||
FXUtils.setValidateWhileTextChanged(txtMaxMemory, true);
|
||||
txtMaxMemory.textProperty().bindBidirectional(maxMemory, SafeStringConverter.fromInteger());
|
||||
FXUtils.bindInt(txtMaxMemory, maxMemory);
|
||||
txtMaxMemory.setValidators(new NumberValidator(i18n("input.number"), false));
|
||||
|
||||
lowerBoundPane.getChildren().setAll(label, slider, txtMaxMemory, new Label("MB"));
|
||||
@@ -628,19 +627,19 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
|
||||
|
||||
// unbind data fields
|
||||
if (lastVersionSetting != null) {
|
||||
FXUtils.unbindInt(txtWidth, lastVersionSetting.widthProperty());
|
||||
FXUtils.unbindInt(txtHeight, lastVersionSetting.heightProperty());
|
||||
FXUtils.unbind(txtWidth, lastVersionSetting.widthProperty());
|
||||
FXUtils.unbind(txtHeight, lastVersionSetting.heightProperty());
|
||||
maxMemory.unbindBidirectional(lastVersionSetting.maxMemoryProperty());
|
||||
javaCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.javaDirProperty());
|
||||
gameDirCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.gameDirProperty());
|
||||
nativesDirCustomOption.valueProperty().unbindBidirectional(lastVersionSetting.nativesDirProperty());
|
||||
FXUtils.unbindString(txtJVMArgs, lastVersionSetting.javaArgsProperty());
|
||||
FXUtils.unbindString(txtGameArgs, lastVersionSetting.minecraftArgsProperty());
|
||||
FXUtils.unbindString(txtMetaspace, lastVersionSetting.permSizeProperty());
|
||||
FXUtils.unbindString(txtWrapper, lastVersionSetting.wrapperProperty());
|
||||
FXUtils.unbindString(txtPreLaunchCommand, lastVersionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbindString(txtPostExitCommand, lastVersionSetting.postExitCommandProperty());
|
||||
FXUtils.unbindString(txtServerIP, lastVersionSetting.serverIpProperty());
|
||||
FXUtils.unbind(txtJVMArgs, lastVersionSetting.javaArgsProperty());
|
||||
FXUtils.unbind(txtGameArgs, lastVersionSetting.minecraftArgsProperty());
|
||||
FXUtils.unbind(txtMetaspace, lastVersionSetting.permSizeProperty());
|
||||
FXUtils.unbind(txtWrapper, lastVersionSetting.wrapperProperty());
|
||||
FXUtils.unbind(txtPreLaunchCommand, lastVersionSetting.preLaunchCommandProperty());
|
||||
FXUtils.unbind(txtPostExitCommand, lastVersionSetting.postExitCommandProperty());
|
||||
FXUtils.unbind(txtServerIP, lastVersionSetting.serverIpProperty());
|
||||
FXUtils.unbindBoolean(chkAutoAllocate, lastVersionSetting.autoMemoryProperty());
|
||||
FXUtils.unbindBoolean(chkFullscreen, lastVersionSetting.fullscreenProperty());
|
||||
noGameCheckPane.selectedProperty().unbindBidirectional(lastVersionSetting.notCheckGameProperty());
|
||||
|
||||
@@ -24,10 +24,7 @@ import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Path;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.PublicKey;
|
||||
import java.security.Signature;
|
||||
import java.security.*;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -63,6 +60,7 @@ public final class IntegrityChecker {
|
||||
|
||||
private static boolean verifyJar(Path jarPath) throws IOException {
|
||||
PublicKey publickey = getPublicKey();
|
||||
MessageDigest md = DigestUtils.getDigest("SHA-512");
|
||||
|
||||
byte[] signature = null;
|
||||
Map<String, byte[]> fileFingerprints = new TreeMap<>();
|
||||
@@ -77,7 +75,8 @@ public final class IntegrityChecker {
|
||||
if (SIGNATURE_FILE.equals(filename)) {
|
||||
signature = IOUtils.readFullyAsByteArray(in);
|
||||
} else {
|
||||
fileFingerprints.put(filename, DigestUtils.digest("SHA-512", in));
|
||||
md.reset();
|
||||
fileFingerprints.put(filename, DigestUtils.digest(md, in));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,7 +90,8 @@ public final class IntegrityChecker {
|
||||
Signature verifier = Signature.getInstance("SHA512withRSA");
|
||||
verifier.initVerify(publickey);
|
||||
for (Entry<String, byte[]> entry : fileFingerprints.entrySet()) {
|
||||
verifier.update(DigestUtils.digest("SHA-512", entry.getKey().getBytes(UTF_8)));
|
||||
md.reset();
|
||||
verifier.update(md.digest(entry.getKey().getBytes(UTF_8)));
|
||||
verifier.update(entry.getValue());
|
||||
}
|
||||
return verifier.verify(signature);
|
||||
|
||||
@@ -94,7 +94,7 @@ public final class UpdateChecker {
|
||||
|
||||
private static boolean isDevelopmentVersion(String version) {
|
||||
return version.contains("@") || // eg. @develop@
|
||||
version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT
|
||||
version.contains("SNAPSHOT"); // eg. 3.5.SNAPSHOT
|
||||
}
|
||||
|
||||
public static void requestCheckUpdate(UpdateChannel channel) {
|
||||
|
||||
@@ -29,12 +29,12 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.UpgradeDialog;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -65,7 +65,7 @@ public final class UpdateHandler {
|
||||
performMigration();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to perform migration", e);
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -75,13 +75,13 @@ public final class UpdateHandler {
|
||||
applyUpdate(Paths.get(args[1]));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to apply update", e);
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE);
|
||||
SwingUtils.showErrorDialog(i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isFirstLaunchAfterUpgrade()) {
|
||||
JOptionPane.showMessageDialog(null, i18n("fatal.migration_requires_manual_reboot"), "Info", JOptionPane.INFORMATION_MESSAGE);
|
||||
SwingUtils.showInfoDialog(i18n("fatal.migration_requires_manual_reboot"));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import org.jackhuang.hmcl.ui.SwingUtils;
|
||||
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.platform.Platform;
|
||||
@@ -55,10 +56,12 @@ import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
@@ -72,6 +75,8 @@ public final class SelfDependencyPatcher {
|
||||
private final List<DependencyDescriptor> dependencies = DependencyDescriptor.readDependencies();
|
||||
private final List<Repository> repositories;
|
||||
private final Repository defaultRepository;
|
||||
private final byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
private final MessageDigest digest = DigestUtils.getDigest("SHA-1");
|
||||
|
||||
private SelfDependencyPatcher() throws IncompatibleVersionException {
|
||||
// We can only self-patch JavaFX on specific platform.
|
||||
@@ -98,14 +103,26 @@ public final class SelfDependencyPatcher {
|
||||
private static final Path DEPENDENCIES_DIR_PATH = HMCL_DIRECTORY.resolve("dependencies").resolve(Platform.getPlatform().toString()).resolve("openjfx");
|
||||
|
||||
static List<DependencyDescriptor> readDependencies() {
|
||||
ArrayList<DependencyDescriptor> dependencies;
|
||||
//noinspection ConstantConditions
|
||||
try (Reader reader = new InputStreamReader(SelfDependencyPatcher.class.getResourceAsStream(DEPENDENCIES_LIST_FILE), UTF_8)) {
|
||||
Map<String, List<DependencyDescriptor>> allDependencies =
|
||||
new Gson().fromJson(reader, new TypeToken<Map<String, List<DependencyDescriptor>>>(){}.getType());
|
||||
return allDependencies.get(Platform.getPlatform().toString());
|
||||
Map<String, ArrayList<DependencyDescriptor>> allDependencies =
|
||||
new Gson().fromJson(reader, new TypeToken<Map<String, ArrayList<DependencyDescriptor>>>(){}.getType());
|
||||
dependencies = allDependencies.get(Platform.getPlatform().toString());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
ClassLoader classLoader = SelfDependencyPatcher.class.getClassLoader();
|
||||
Class.forName("netscape.javascript.JSObject", false, classLoader);
|
||||
Class.forName("org.w3c.dom.html.HTMLDocument", false, classLoader);
|
||||
} catch (Throwable e) {
|
||||
LOG.log(Level.WARNING, "Disable javafx.web because JRE is incomplete", e);
|
||||
dependencies.removeIf(it -> "javafx.web".equals(it.module) || "javafx.media".equals(it.module));
|
||||
}
|
||||
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
public String module;
|
||||
@@ -261,9 +278,10 @@ public final class SelfDependencyPatcher {
|
||||
* @throws IOException When the files cannot be fetched or saved.
|
||||
*/
|
||||
private void fetchDependencies(List<DependencyDescriptor> dependencies) throws IOException {
|
||||
SwingUtils.initLookAndFeel();
|
||||
|
||||
boolean isFirstTime = true;
|
||||
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
Repository repository = defaultRepository;
|
||||
|
||||
int count = 0;
|
||||
@@ -360,8 +378,18 @@ public final class SelfDependencyPatcher {
|
||||
return missing;
|
||||
}
|
||||
|
||||
private static void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
ChecksumMismatchException.verifyChecksum(dependency.localPath(), "SHA-1", dependency.sha1());
|
||||
private void verifyChecksum(DependencyDescriptor dependency) throws IOException, ChecksumMismatchException {
|
||||
digest.reset();
|
||||
try (InputStream is = Files.newInputStream(dependency.localPath())) {
|
||||
int read;
|
||||
while ((read = is.read(buffer, 0, IOUtils.DEFAULT_BUFFER_SIZE)) > -1) {
|
||||
digest.update(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
|
||||
String sha1 = Hex.encodeHex(digest.digest());
|
||||
if (!dependency.sha1().equalsIgnoreCase(sha1))
|
||||
throw new ChecksumMismatchException("SHA-1", dependency.sha1(), sha1);
|
||||
}
|
||||
|
||||
public static class PatchException extends Exception {
|
||||
|
||||
@@ -133,10 +133,10 @@ fi
|
||||
|
||||
if [[ "$_HMCL_ARCH" == "loongarch64" ]]; then
|
||||
if [ "$_HMCL_USE_CHINESE" == true ]; then
|
||||
echo "运行 HMCL 需要 Java 运行时环境,请安装龙芯 JDK8 (http://www.loongnix.cn/zh/api/java/downloads-jdk8/index.html) 并设置环境变量后重试。" 1>&2
|
||||
echo "运行 HMCL 需要 Java 运行时环境,请安装龙芯 JDK8 (https://docs.hmcl.net/downloads/loongnix.html) 并设置环境变量后重试。" 1>&2
|
||||
else
|
||||
echo "The Java runtime environment is required to run HMCL." 1>&2
|
||||
echo "Please install Loongson JDK8 (http://www.loongnix.cn/zh/api/java/downloads-jdk8/index.html) and set the environment variables, then try again." 1>&2
|
||||
echo "Please install Loongson JDK8 (https://docs.hmcl.net/downloads/loongnix.html) and set the environment variables, then try again." 1>&2
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
@@ -144,11 +144,11 @@ fi
|
||||
|
||||
case "$_HMCL_OS" in
|
||||
linux)
|
||||
_HMCL_DOWNLOAD_PAGE_OS="&os=Linux";;
|
||||
_HMCL_DOWNLOAD_PAGE_OS="linux";;
|
||||
osx)
|
||||
_HMCL_DOWNLOAD_PAGE_OS="&os=macOS";;
|
||||
_HMCL_DOWNLOAD_PAGE_OS="macos";;
|
||||
windows)
|
||||
_HMCL_DOWNLOAD_PAGE_OS="&os=Windows";;
|
||||
_HMCL_DOWNLOAD_PAGE_OS="windows";;
|
||||
*)
|
||||
echo "Unknown os: $_HMCL_OS" 1>&2
|
||||
exit 1
|
||||
@@ -157,20 +157,20 @@ esac
|
||||
|
||||
case "$_HMCL_ARCH" in
|
||||
arm64)
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="&bitness=64-bit&architecture=ARM";;
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="arm64";;
|
||||
arm32)
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="&bitness=32-bit&architecture=ARM";;
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="arm32";;
|
||||
x86_64)
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="&bitness=64-bit&architecture=x86";;
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="x86_64";;
|
||||
x86)
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="&bitness=32-bit&architecture=x86";;
|
||||
_HMCL_DOWNLOAD_PAGE_ARCH="x86";;
|
||||
*)
|
||||
echo "Unknown architecture: $_HMCL_ARCH" 1>&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
_HMCL_DOWNLOAD_PAGE="https://bell-sw.com/pages/downloads/?version=java-17-lts$_HMCL_DOWNLOAD_PAGE_OS$_HMCL_DOWNLOAD_PAGE_ARCH&package=jdk-full"
|
||||
_HMCL_DOWNLOAD_PAGE="https://docs.hmcl.net/downloads/$_HMCL_DOWNLOAD_PAGE_OS/$_HMCL_DOWNLOAD_PAGE_ARCH.html"
|
||||
|
||||
if [ "$_HMCL_USE_CHINESE" == true ]; then
|
||||
echo "运行 HMCL 需要 Java 运行时环境,请安装 Java 并设置环境变量后重试。" 1>&2
|
||||
@@ -180,4 +180,4 @@ else
|
||||
echo "Please install Java and set the environment variables and try again." 1>&2
|
||||
echo "$_HMCL_DOWNLOAD_PAGE" 1>&2
|
||||
fi
|
||||
exit 1
|
||||
exit 1
|
||||
@@ -18,7 +18,7 @@
|
||||
<JFXTextField fx:id="txtServerUrl" promptText="%account.injector.server_url" onAction="#onAddNext" />
|
||||
</body>
|
||||
<actions>
|
||||
<Label fx:id="lblCreationWarning" />
|
||||
<Label fx:id="lblCreationWarning" wrapText="true" />
|
||||
<HBox>
|
||||
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
|
||||
<SpinnerPane fx:id="nextPane" styleClass="small-spinner-pane">
|
||||
|
||||
@@ -1,38 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<?import java.lang.String?>
|
||||
<?import javafx.geometry.Insets?>
|
||||
<fx:root xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
type="StackPane">
|
||||
<BorderPane styleClass="jfx-dialog-layout">
|
||||
<left>
|
||||
<Label fx:id="graphic" translateX="10" translateY="10" minWidth="40" maxWidth="40" minHeight="40" maxHeight="40" />
|
||||
</left>
|
||||
<center>
|
||||
<VBox>
|
||||
<BorderPane.margin>
|
||||
<Insets left="8" />
|
||||
</BorderPane.margin>
|
||||
<HBox spacing="8">
|
||||
<StackPane>
|
||||
<styleClass>
|
||||
<String fx:value="jfx-layout-heading" />
|
||||
<String fx:value="title" />
|
||||
</styleClass>
|
||||
<Label fx:id="title" text="%message.info" />
|
||||
</StackPane>
|
||||
</HBox>
|
||||
<ScrollPane VBox.vgrow="ALWAYS" fitToWidth="true" fitToHeight="true">
|
||||
<StackPane fx:id="content" styleClass="jfx-layout-body" />
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
</center>
|
||||
<bottom>
|
||||
<HBox fx:id="actions" styleClass="jfx-layout-actions" />
|
||||
</bottom>
|
||||
</BorderPane>
|
||||
<HBox spacing="8" styleClass="jfx-dialog-layout">
|
||||
<Label fx:id="graphic" translateX="10" translateY="10" minWidth="40" maxWidth="40" minHeight="40"
|
||||
maxHeight="40"/>
|
||||
<VBox HBox.hgrow="ALWAYS">
|
||||
<StackPane>
|
||||
<styleClass>
|
||||
<String fx:value="jfx-layout-heading"/>
|
||||
<String fx:value="title"/>
|
||||
</styleClass>
|
||||
<Label fx:id="title" text="%message.info"/>
|
||||
</StackPane>
|
||||
<StackPane fx:id="content" styleClass="jfx-layout-body"/>
|
||||
<HBox fx:id="actions" styleClass="jfx-layout-actions"/>
|
||||
</VBox>
|
||||
</HBox>
|
||||
</fx:root>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 867 B |
|
After Width: | Height: | Size: 999 B |
|
After Width: | Height: | Size: 912 B |
|
After Width: | Height: | Size: 1001 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 861 B |
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 947 B |
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -21,16 +21,16 @@
|
||||
|
||||
about=About
|
||||
about.copyright=Copyright
|
||||
about.copyright.statement=Copyright (c) 2022 huangyuhui.
|
||||
about.copyright.statement=Copyright © 2022 huangyuhui.
|
||||
about.author=Author
|
||||
about.author.statement=@huanghongxun on Bilibili
|
||||
about.author.statement=@huanghongxun on bilibili
|
||||
about.claim=EULA
|
||||
about.claim.statement=Click on this link for full text.
|
||||
about.community=Community
|
||||
about.community.discord=Discord
|
||||
about.community.discord.statement=Join our Discord community\!
|
||||
about.community.discord.statement=Join our Discord community !
|
||||
about.community.kookapp=KOOK
|
||||
about.community.kookapp.statement=Join our KOOK community\!
|
||||
about.community.kookapp.statement=Join our KOOK community !
|
||||
about.dependency=Third-party Libraries
|
||||
about.legal=Legal Acknowledgement
|
||||
about.thanks_to=Thanks to
|
||||
@@ -43,8 +43,6 @@ about.thanks_to.mcbbs=MCBBS (Minecraft Chinese Forum)
|
||||
about.thanks_to.mcbbs.statement=For providing mcbbs.net download mirror for Mainland China users.
|
||||
about.thanks_to.mcmod=mcmod.cn
|
||||
about.thanks_to.mcmod.statement=For providing Chinese translations and wiki for various mods.
|
||||
about.thanks_to.noin=阿猫 (mcer.cn)
|
||||
about.thanks_to.noin.statement=For providing multiplayer service.
|
||||
about.thanks_to.red_lnn.statement=For providing the default background image.
|
||||
about.thanks_to.users=HMCL User Group Members
|
||||
about.thanks_to.users.statement=Thanks for donations, bug reports, and so on.
|
||||
@@ -62,7 +60,7 @@ account.create.yggdrasil=Add a Mojang account
|
||||
account.create.offline=Add an offline account
|
||||
account.create.authlibInjector=Create an authlib-injector account
|
||||
account.email=Email
|
||||
account.failed=Failed to login
|
||||
account.failed=Account refresh failed
|
||||
account.failed.character_deleted=The character has already been deleted.
|
||||
account.failed.connect_authentication_server=Unable to contact authentication servers, your Internet connection may be down.
|
||||
account.failed.connect_injector_server=Unable to connect to the authentication server. Please check your network and make sure you entered the correct URL.
|
||||
@@ -87,7 +85,8 @@ account.injector.server_url=Server URL
|
||||
account.injector.server_name=Server Name
|
||||
account.login=Login
|
||||
account.login.hint=We will not store your password.
|
||||
account.login.offline=Login offline
|
||||
account.login.skip=Login offline
|
||||
account.login.retry=Retry
|
||||
account.login.refresh=Re-login
|
||||
account.login.refresh.microsoft.hint=<b>Because the account authorization is invalid, you need to re-add your Microsoft account</b>\nPlease click on the "OK" button, and copy the code shown here later to finish the login process in the opened browser window.
|
||||
account.login.refresh.microsoft.manual=<b>Because the account authorization is invalid, you need to re-add your Microsoft account</b>\nYour device code is <b>%1$s</b>, please click here to copy. After clicking on the "OK" button, you should finish the login process in the opened browser window. If it did not show, you can go to %2$s manually.
|
||||
@@ -121,12 +120,14 @@ account.methods.microsoft.profile=Account Profile
|
||||
account.methods.microsoft.snapshot=HMCL Snapshot version does not support Microsoft login, please use the stable version or beta version to log in.
|
||||
account.methods.microsoft.waiting_browser=Waiting for login to complete in the opened window...
|
||||
account.methods.offline=Offline
|
||||
account.methods.offline.name.special_characters=Recommended to use letters, numbers and underscores for naming
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUID is the unique identifier for the game character in Minecraft. The way that it is generated might vary between different game launchers. Changing it to the one generated by other launchers allows you to keep your items in your offline account inventory. \n\
|
||||
\n\
|
||||
This option is for advanced users only. We do not recommend modifying this option unless you know what you are doing.
|
||||
account.methods.offline.uuid.malformed=Invalid Format
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.methods.forgot_password=Forgot Password
|
||||
account.methods.yggdrasil.migration=Migrate to Microsoft Account
|
||||
account.methods.yggdrasil.migration.how=How to migrate to Microsoft Account?
|
||||
account.methods.yggdrasil.migration.hint=Since March 10, 2022, Mojang accounts are no longer supported, and you are required to migrate to a Microsoft account in order to play Minecraft.\n\
|
||||
@@ -166,6 +167,7 @@ assets.index.malformed=Index files of downloaded assets were corrupted. You can
|
||||
button.cancel=Cancel
|
||||
button.change_source=Change Download Source
|
||||
button.clear=Clear
|
||||
button.copy_and_exit=Copy and Exit
|
||||
button.delete=Delete
|
||||
button.edit=Edit
|
||||
button.install=Install
|
||||
@@ -231,6 +233,7 @@ curse.category.4486=Lucky Blocks
|
||||
curse.category.432=Buildcraft
|
||||
curse.category.418=Genetics
|
||||
curse.category.4671=Twitch Integration
|
||||
curse.category.5314=KubeJS
|
||||
curse.category.408=Ores and Resources
|
||||
curse.category.4773=CraftTweaker
|
||||
curse.category.430=Thaumcraft
|
||||
@@ -307,6 +310,7 @@ download=Download
|
||||
download.code.404=File not found on the remote server\: %s
|
||||
download.content=Addons
|
||||
download.curseforge.customization=Light and shadow, and game customization
|
||||
download.curseforge.unavailable=HMCL nightly build does not support access to CurseForge, please use stable version or beta version to download.
|
||||
download.existing=The file cannot be saved because it already exists. You can use 'Save As' to save the file elsewhere.
|
||||
download.external_link=Open Website
|
||||
download.failed=Failed to download %1$s, response code\: %2$d
|
||||
@@ -354,16 +358,19 @@ fatal.missing_dst_root_ca_x3=The DST Root CA X3 certificate is missing from the
|
||||
You can still use Hello Minecraft\! Launcher, but it will be unable to connect to some websites (such as sites that use certificates issued by Let's Encrypt), which may cause the launcher to work improperly.\n\
|
||||
\n\
|
||||
Please update your Java version to at least 8u101 to fix this issue.
|
||||
fatal.config_change_owner_root=You are using the root account to start Hello Minecraft\! Launcher, this may cause you to fail to start Hello Minecraft\! Launcher with other account in the future.\n\
|
||||
Do you still want to continue?
|
||||
fatal.config_loading_failure=Cannot load configuration files.\n\
|
||||
Please make sure that "Hello Minecraft\! Launcher" has read and write access to "%s" and the files in it.
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher could not load the profile because the profile was created by user %1$s. \n\
|
||||
Please start HMCL with sudo (not recommended), or execute the following command in the terminal to change the ownership of the configuration file to the current user:\n%2$s
|
||||
fatal.migration_requires_manual_reboot=Hello Minecraft\! Launcher has been upgraded. Please reopen the launcher.
|
||||
fatal.apply_update_failure=We are sorry, but Hello Minecraft\! Launcher is unable to update.\n\
|
||||
\n\
|
||||
You can update manually by downloading a newer version of the launcher from %s.\n\
|
||||
If the problem persists, please consider reporting this to us.
|
||||
fatal.samba=If you launched HMCL from a Samba network drive, some features might not be working. Please try updating your Java version or copy and run the launcher in a local folder.
|
||||
fatal.illegal_char=Your user path contains an illegal character '\=', so some features might not function properly.\n\
|
||||
For example, you will not be able to use authlib-injector or change the skin of your offline account. Besides, this may cause security issues during multiplayer.
|
||||
fatal.illegal_char=Your user path contains an illegal character '\=', you will not be able to use authlib-injector or change the skin of your offline account.
|
||||
fatal.unsupported_platform=Minecraft is not yet fully supported for your platform, so you may experience missing functionality, \nor even be unable to launch the game.\n\
|
||||
\n\
|
||||
If you can't start Minecraft 1.17 and above, you can try turning on the "Use OpenGL software renderer" option\n\
|
||||
@@ -466,6 +473,10 @@ You can go to <a href\="https\://hmcl.huangyuhui.net/help/game/graphics-card">th
|
||||
game.crash.reason.illegal_access_error=The game crashed because of some mod(s).\n\
|
||||
\n\
|
||||
If you know\: %1$s, you can update or delete the mod(s) and then try again.
|
||||
game.crash.reason.optifine_is_not_compatible_with_forge=The current game crashes because OptiFine is incompatible with the current version of Forge. \n Please go to<a href="https://optifine.net/downloads">On the official website of OptiFine</a>, check the Forge version compatible with OptiFine, and reinstall the game in strict accordance with the corresponding version or change the version in the version settings - automatic installation. nAfter testing, too high or too low a Forge version may cause a crash.
|
||||
game.crash.reason.mod_files_are_decompressed=The current game cannot continue to run because the Mod file has been decompressed. \n Please put the entire Mod file directly into the Mod folder! \n If unzipping will cause errors in the game, please delete the unzipped Mod in the Mod folder, and then start the game.
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=The current game cannot continue to run because you have installed too many Mods, which exceeds the ID limit of the game. \n Please try installing<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>, or delete some large Mods.
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=The current game may not continue to run because of OptiFine. \n This problem only occurs in a specific version of OptiFine. You can try to change the version of OptiFine in version setting - automatic installation.
|
||||
game.crash.reason.jdk_9=The game cannot run because the Java version is too new for this instance.\n\
|
||||
\n\
|
||||
You need to download and install Java 8 and select it in the instance settings.
|
||||
@@ -480,6 +491,9 @@ You can try to delete or update it.
|
||||
game.crash.reason.loading_crashed_fabric=The game crashed due to mod %1$s.\n\
|
||||
\n\
|
||||
You can try to delete or update it.
|
||||
game.crash.reason.mac_jdk_8u261==The game crashed because your current Forge or OptiFine version is not compatible with your Java installation.\n\
|
||||
\n\
|
||||
Please try to update Forge and OptiFine, or try to use Java 8u251 or earlier versions.
|
||||
game.crash.reason.memory_exceeded=The game crashed due to too much memory allocated for a small page file.\n\
|
||||
\n\
|
||||
You can try to turn off the automatically allocate memory option in settings, and adjust the value till the game launches.\n\
|
||||
@@ -547,6 +561,7 @@ If you still encounter these problems, well, you may need a better computer.
|
||||
game.crash.reason.resolution_too_high=The game crashed because you are using a resource pack whose texture resolution was too high.\n\
|
||||
\n\
|
||||
You should switch to a resource pack with lower resolution, or consider buying a better graphics card with more VRAM.
|
||||
game.crash.reason.processing_of_javaagent_failed=The current game crashed because processing of -javaagent failed.\n If you add relevant parameters to the Java virtual machine parameters, please check whether they are legal and correct.\n If you do not add relevant parameters or confirm that they are legal and correct, Please try: \n Open the control panel -- Clock and region classification (this option is only available if the option is category display, and it will be skipped if not) -- Region -- the upper management tab -- the lower change system regional setting button -- turn off the "Use Unicode UTF-8 to provide global language support" option in the pop-up window, restart the device, and then try to start the game. \n You can be accessed in<a href=“https://discord.gg/jVvC7HfM6U">Discard</a>or<a href="https://kook.top/Kx7n3t">KOOK</a>ask for help.
|
||||
game.crash.reason.stacktrace=The crash reason is unknown. You can view its details by clicking the "Logs" button.\n\
|
||||
\n\
|
||||
There are some keywords that might contain some Mod IDs. You can search them online to figure out the issue yourself.\n\
|
||||
@@ -607,7 +622,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Not Selected
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=Install a Modpack
|
||||
install.new_game=Add a New Instance
|
||||
@@ -672,7 +687,7 @@ launcher.agreement.hint=You must agree to the EULA to use this software.
|
||||
launcher.background=Background Image
|
||||
launcher.background.choose=Choose a Background Image
|
||||
launcher.background.classic=Classic
|
||||
launcher.background.default=Default (or background.png/jpg, or images under bg folder)
|
||||
launcher.background.default=Default (or background.png/jpg/gif, or images under bg folder)
|
||||
launcher.background.network=From URL
|
||||
launcher.background.translucent=Translucent
|
||||
launcher.cache_directory=Cache Directory
|
||||
@@ -785,28 +800,43 @@ Keep in mind that you are not allowed to add mods and resource packs that are ex
|
||||
modpack.wizard.step.initialization.server=Click here for more tutorials for making a server modpack that can be automatically updated.
|
||||
|
||||
modrinth.category.adventure=Adventure
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=Challenging
|
||||
modrinth.category.combat=Combat
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=Decoration
|
||||
modrinth.category.economy=Economy
|
||||
modrinth.category.equipment=Equipment
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.food=Food
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=Game Mechanics
|
||||
modrinth.category.kitchen-sink=Kitchen-Sink
|
||||
modrinth.category.library=Library
|
||||
modrinth.category.lightweight=Lightweight
|
||||
modrinth.category.magic=Magic
|
||||
modrinth.category.management=Management
|
||||
modrinth.category.minigame=Minigame
|
||||
modrinth.category.misc=Misc
|
||||
modrinth.category.mobs=Mobs
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=Multiplayer
|
||||
modrinth.category.optimization=Optimization
|
||||
modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=Quests
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.social=Social
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=Storage
|
||||
modrinth.category.technology=Technology
|
||||
modrinth.category.transportation=Transportation
|
||||
modrinth.category.utility=Utility
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=Worldgen
|
||||
|
||||
mods=Mods
|
||||
@@ -847,23 +877,45 @@ multiplayer.download.success=Multiplayer online initialization completed
|
||||
multiplayer.download.unsupported=Multiplayer dependencies are not supported on current system or platform
|
||||
multiplayer.error.failed_get_device=HiPer could not create a network device, maybe HiPer has already started or lacks administrator privileges.
|
||||
multiplayer.error.failed_load_config=HiPer initialization failed, maybe there is a problem with the configuration file or the certificate is invalid.
|
||||
multiplayer.error.file_not_found=The HiPer program could not be found. The program should finish downloading when entering the multiplayer page. Please restart HMCL and try again. \nPlease check if your computer's antivirus software has flagged HiPer as a virus, and if so, restore HiPer.
|
||||
multiplayer.error.failed_sudo.linux=HiPer requires administrator privileges. You can configure how HiPer applies for privileges by setting the environment variable HMCL_ASKPASS. \nIf HMCL_ASKPASS is set to 'false', please configure sudoers file to allow '%s' to obtain root privileges without password, then restart the computer and start HMCL again;\nIf HMCL_ASKPASS is set to 'user', please set the environment variable SUDO_ASKPASS first, then HMCL will start HiPer with 'sudo --askpass' (see man sudo for details).
|
||||
multiplayer.error.failed_sudo.mac=HiPer requires administrator privileges. Do you want to grant HiPer administrator rights? \n Click "Yes" to grant HiPer administrator permission, and then restart your computer and open HMCL again.
|
||||
multiplayer.error.failed_sudo.windows=HiPer requires administrator privileges, please restart HMCL with administrator privileges. Would you like to see the tutorial (Simplified Chinese only)?
|
||||
multiplayer.error.file_not_found=An update to HiPer has been found. Please re-enter the multiplayer online page to update HiPer.
|
||||
multiplayer.error.session_expired=Current multiplayer session has been expired. Please fetch a new multiplayer token.
|
||||
multiplayer.exit=HiPer exited unexpectedly with exit code %d
|
||||
multiplayer.hint=The multiplayer online function is in the experimental stage, if you have any questions, please go to mcer.cn to give feedback
|
||||
multiplayer.powered_by=This service is provided under the license of (<a href="https://mcer.cn">Matrix Lab</a>)<a href\="https\://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement" >License Agreement</a>
|
||||
multiplayer.help.1=管理员权限打开
|
||||
multiplayer.help.2=多人联机教程
|
||||
multiplayer.help.3=创建方帮助
|
||||
multiplayer.help.4=参与者帮助
|
||||
multiplayer.help.text=测试能否联机
|
||||
multiplayer.hint=Multiplayer online function is under maintenance.
|
||||
multiplayer.hint.details=View details
|
||||
multiplayer.persistence=License Management
|
||||
multiplayer.persistence.export=Export license to file
|
||||
multiplayer.persistence.export.button=Export license file
|
||||
multiplayer.persistence.export.failed=Failed to export the license file, please check the save path you selected.
|
||||
multiplayer.persistence.export.file_not_exists=Failed to obtain license file, please check whether the token is entered correctly.
|
||||
multiplayer.persistence.export.title=Select the path to save the license file
|
||||
multiplayer.persistence.hint=This feature is available for long-term license (valid for more than 24 hours) users, if you are using short-term license (valid for less than 24 hours), you do not need to use this feature.\nWhen you use a long-term license, please use the token to start HiPer within 24 hours after obtaining the license, otherwise HiPer will not be able to obtain the license information. \nAfterwards, you can save the license file through the "Export License File" function below. \nPlease keep the license file in a safe place. After 24 hours of authorization, this file is the only license for your. If you lose this file, you will not be able to play multiplayer games through the token.
|
||||
multiplayer.persistence.import=Import license from file
|
||||
multiplayer.persistence.import.button=Import license file
|
||||
multiplayer.persistence.import.failed=Failed to import license file.
|
||||
multiplayer.persistence.import.file_already_exists=The license file already exists.
|
||||
multiplayer.persistence.import.title=Choose license file
|
||||
multiplayer.persistence.import.token_not_match=The license file you selected may not match the token.\nDo you still want to continue?
|
||||
multiplayer.persistence.license_file=License file
|
||||
multiplayer.powered_by=This service is provided under the license of (<a href="https://mcer.cn">Matrix Lab</a>)<a href\="https://docs.hmcl.net/multiplayer/#%E4%BD%BF%E7%94%A8%E6%8E%88%E6%9D%83" >License Agreement</a>
|
||||
multiplayer.report=Illegal and Violation Report
|
||||
multiplayer.session.name.motd=HMCL Multiplayer Session
|
||||
multiplayer.token=Token
|
||||
multiplayer.token.apply=Apply for a token
|
||||
multiplayer.token.expired=HiPer certificate expired, please restart HMCL and try again.
|
||||
multiplayer.token.invalid=Invalid credentials.
|
||||
multiplayer.token.expired=HiPer token expired, please restart HMCL and try again.
|
||||
multiplayer.token.format_invalid=Invalid token format
|
||||
multiplayer.token.invalid=Invalid token.
|
||||
multiplayer.token.malformed=HiPer configuration file could not be parsed, please restart HMCL and try again.
|
||||
multiplayer.token.prompt=You need a token to use the multiplayer service. Click on the "Application Credentials" next to it to view the details
|
||||
multiplayer.off=Start HiPer
|
||||
multiplayer.off.hint=The multiplayer service is provided by HiPer, you need to start HiPer first to use the multiplayer service.
|
||||
multiplayer.off.hint=Due to the need to register system network service permissions, administrator rights will be requested when starting HiPer. If prompted for authentication, please allow to start HiPer.
|
||||
multiplayer.off.start=Start HiPer
|
||||
multiplayer.on=HiPer
|
||||
multiplayer.master=Prompt for creator
|
||||
multiplayer.master.hint=Multiplayer online requires one player to start the game first, select the single player mode to enter a save, and select the "Open to LAN" option in the game options menu. After that you can see the port number (usually 5 digits) indicated by the game in the game chat box. Click the Generate Server Address button below, enter the port number, and you can get your server address. This address needs to be provided to other players who need to join the server to add the server.
|
||||
multiplayer.master.server_address=Generate server address
|
||||
@@ -872,12 +924,13 @@ multiplayer.master.video_tutorial=Video Tutorial
|
||||
multiplayer.master.port=Port number
|
||||
multiplayer.master.port.validate=The port number (0~65535) displayed in the game chat box,when you open the game in LAN.
|
||||
multiplayer.slave=Participant Prompt
|
||||
multiplayer.slave.hint=If you want to join another player's game save to play the game, you need to ask that player to turn on the open mode to the local area network according to the operation prompted by the creator. Then start the game, and select the multiplayer mode, select Add Server. The game will ask you to enter the server address, you only need to create a player to ask for the server address and enter it, and then enter the server.
|
||||
multiplayer.slave.hint=1.Ask the creator to follow the instructions above\n2.Start the game\n3.Choose multiplayer mode and add a server\n4.The game will require you to enter the server address. You only need to ask the creator for the server address and enter it into the server.
|
||||
multiplayer.slave.hint2=1. In general, the participant's game account must be a Microsoft account or an external login account (such as Little Skin), otherwise you need to enter the server address into the input box below and click "Join", select the multiplayer mode in the game, enter the LAN world before you can join \n2. In general, the participant's game version and module must be the same as the creator, otherwise join fails.
|
||||
multiplayer.slave.server_address=Creator server address
|
||||
multiplayer.slave.server_address.start=Join
|
||||
multiplayer.slave.server_address.stop=Exit
|
||||
multiplayer.slave.video_tutorial=Video Tutorial
|
||||
multiplayer.session.expiration=Expire Time
|
||||
multiplayer.session.expiration=Token Expiration Time
|
||||
|
||||
datapack=Datapacks
|
||||
datapack.add=Install datapack
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
about=Acerca de
|
||||
about.copyright=Copyright
|
||||
about.copyright.statement=Copyright (c) 2022 huangyuhui.
|
||||
about.copyright.statement=Copyright © 2022 huangyuhui.
|
||||
about.author=Autor
|
||||
about.author.statement=@huanghongxun en Bilibili
|
||||
about.author.statement=@huanghongxun en bilibili
|
||||
about.claim=EULA
|
||||
about.claim.statement=Haz clic en este enlace para ver el texto completo.
|
||||
about.community=Comunidad
|
||||
@@ -43,14 +43,12 @@ about.thanks_to.mcbbs=MCBBS (Foro chino de Minecraft)
|
||||
about.thanks_to.mcbbs.statement=Por proporcionar un espejo de descarga de mcbbs.net para los usuarios de China continental.
|
||||
about.thanks_to.mcmod=mcmod.cn
|
||||
about.thanks_to.mcmod.statement=Por facilitar las traducciones al chino y el wiki de varios mods.
|
||||
about.thanks_to.noin=阿猫 (mcer.cn)
|
||||
about.thanks_to.noin.statement=Por proporcionar el servicio multijugador.
|
||||
about.thanks_to.red_lnn.statement=Por proporcionar la imagen de fondo por defecto.
|
||||
about.thanks_to.users=Miembros del grupo de usuarios de HMCL
|
||||
about.thanks_to.users.statement=Gracias por las donaciones, los informes de errores, etc.
|
||||
about.thanks_to.yushijinhun.statement=Por proporcionar soporte relacionado con authlib-injector.
|
||||
about.open_source=Código abierto
|
||||
about.open_source.statement=GPL v3 (https\://github.com/huanghongxun/HMCL)
|
||||
about.open_source.statement=GPL v3 (https://github.com/huanghongxun/HMCL)
|
||||
|
||||
account=Cuentas
|
||||
account.cape=Capa
|
||||
@@ -123,6 +121,7 @@ account.methods.offline.uuid.hint=UUID es el identificador único del personaje
|
||||
Esta opción es sólo para usuarios avanzados. No recomendamos modificar esta opción a menos que sepas lo que estás haciendo.
|
||||
account.methods.offline.uuid.malformed=Formato no válido
|
||||
account.methods.yggdrasil=Mojang
|
||||
account.methods.forgot_password=OLVIDÉ MI CONTRASEÑA
|
||||
account.methods.yggdrasil.migration=Migrar a cuenta Microsoft
|
||||
account.methods.yggdrasil.migration.how=¿Cómo migrar a la Cuenta Microsoft?
|
||||
account.methods.yggdrasil.migration.hint=Desde el 10 de marzo de 2022, las cuentas de Mojang ya no son compatibles, y es necesario migrar a una cuenta de Microsoft para poder jugar a Minecraft.\n
|
||||
@@ -355,7 +354,7 @@ Usted puede actualizar manualmente mediante la descarga de una versión más rec
|
||||
Si el problema persiste, por favor considere reportarlo a nosotros.
|
||||
fatal.samba=Si ha iniciado HMCL desde una unidad de red Samba, es posible que algunas funciones no funcionen. Por favor, intente actualizar su versión de Java o copie y ejecute el launcher en una carpeta local.
|
||||
fatal.illegal_char=Su ruta de usuario contiene un carácter ilegal '\=', por lo que algunas características podrían no funcionar correctamente.\n\
|
||||
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline. Además, esto puede causar problemas de seguridad durante el multijugador.
|
||||
Por ejemplo, no podrá utilizar authlib-injector o cambiar el skin de su cuenta offline.
|
||||
|
||||
feedback=Comentarios
|
||||
feedback.add=Añadir un comentario
|
||||
@@ -592,7 +591,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=No seleccionado
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=Instalar un Modpack
|
||||
install.new_game=Añadir instancia
|
||||
@@ -660,7 +659,7 @@ launcher.agreement.hint=Debe aceptar el EULA para utilizar este software.
|
||||
launcher.background=Imagen de fondo
|
||||
launcher.background.choose=Elige una imagen de fondo
|
||||
launcher.background.classic=Clásico
|
||||
launcher.background.default=Por defecto (o background.png/jpg, o imágenes en la carpeta bg)
|
||||
launcher.background.default=Por defecto (o background.png/jpg/gif, o imágenes en la carpeta bg)
|
||||
launcher.background.network=Desde la URL
|
||||
launcher.background.translucent=Translúcido
|
||||
launcher.cache_directory=Directorio de la caché
|
||||
@@ -841,7 +840,6 @@ O bien, puedes esperar unos minutos. Si la función sigue sin funcionar, consult
|
||||
multiplayer.exit.timeout.dynamic_token=No se ha podido conectar al servicio multijugador, los servidores de autenticación pueden tener demasiadas peticiones en ese momento. Por favor, espere hasta el próximo medio tiempo para permitir que los servidores despejen algunas sesiones.\n\
|
||||
\n\
|
||||
También puedes comprar algunos tokens multijugador para conseguir una experiencia multijugador estable.
|
||||
multiplayer.hint=La función multijugador está actualmente en fase alfa. Por favor, proporciona tus comentarios en mcer.cn.
|
||||
multiplayer.nat=Detección de red
|
||||
multiplayer.nat.failed=No se ha podido detectar el tipo de red.
|
||||
multiplayer.nat.hint=La detección del tipo de red comprobará si su entorno de red es compatible con la funcionalidad multijugador.\n\
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
# Contributors: zhixuan2333
|
||||
about=About
|
||||
about.copyright=著作権
|
||||
about.copyright.statement=Copyright(c)2021huangyuhui。
|
||||
about.copyright.statement=Copyright © 2022 huangyuhui。
|
||||
about.author=Author
|
||||
about.author.statement=Bilibili: @huanghongxun
|
||||
about.author.statement=bilibili : @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=テキスト全体のリンクをクリックします。
|
||||
about.community=コミュニティ
|
||||
@@ -41,8 +41,6 @@ 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=mcer.cn
|
||||
about.thanks_to.noin.statement=マルチプレイヤーサービスプロバイダー。
|
||||
about.thanks_to.red_lnn.statement=デフォルトの背景画像プロバイダー。
|
||||
about.thanks_to.users=HMCLユーザーグループのメンバー
|
||||
about.thanks_to.users.statement=寄付、バグレポートなどに感謝します。
|
||||
@@ -102,6 +100,7 @@ account.methods.microsoft.error.missing_xbox_account=Microsoftアカウントが
|
||||
account.methods.microsoft.error.no_character=アカウントにMinecraftJavaプロファイルがありません。\nゲームファイルが作成されていない可能性があります\n上記のリンクをクリックして作成してください
|
||||
account.methods.microsoft.error.unknown=ログインに失敗しました。Microsoftはエラーコード %d で応答します。
|
||||
account.methods.microsoft.logging_in=ログイン...
|
||||
account.methods.forgot_password=パスワードをお忘れの方
|
||||
account.methods.microsoft.makegameidsettings=プロファイルを作成
|
||||
account.methods.microsoft.hint=「ログイン」ボタンをクリックして、新しく開いたブラウザウィンドウでログインプロセスを続行する必要があります。\nMicrosoftアカウントへのログインに使用されたトークンが誤って漏洩した場合は、下の[アカウントのバインドを解除]をクリックして、ログイン認証をキャンセルできます。
|
||||
account.methods.microsoft.manual=「ログイン」ボタンをクリックした後、新しく開いたブラウザウィンドウで認証を完了する必要があります。ブラウザウィンドウが表示されない場合は、ここをクリックしてURLをコピーし、ブラウザで手動で開くことができます。\nMicrosoftアカウントへのログインに使用されたトークンが誤って漏洩した場合は、下の[アカウントのバインドを解除]をクリックして、ログイン認証をキャンセルできます。
|
||||
@@ -321,7 +320,7 @@ fatal.config_loading_failure=構成にアクセスできません。\nHelloMinec
|
||||
fatal.migration_requires_manual_reboot=更新が完了しました。 Hello Minecraftを再開してください!ランチャー。
|
||||
fatal.apply_update_failure=ごめんなさい、Hello Minecraft! Launcher 何か問題が発生したため、ランチャーは更新を完了できませんでした。\nただし、Hello Minecraftをダウンロードすることで、手動で更新を終了できます。 %s からのランチャー。\nこの問題を報告することを検討してください。
|
||||
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.
|
||||
fatal.illegal_char=ユーザーフォルダーのパスに不正な文字'='が含まれています。hmclは動作しますが、一部の機能が正しく動作しません。 \nログインアカウントやオフラインログインではスキンの変更ができなくなり、多人数参加型ゲームをプレイする際のセキュリティリスクが高くなる可能性があります。
|
||||
fatal.illegal_char=ユーザーフォルダーのパスに不正な文字'='が含まれています, ログインアカウントやオフラインログインではスキンの変更ができなくなり。
|
||||
|
||||
feedback=フィードバック
|
||||
Feedback.add=フィードバックを追加
|
||||
@@ -448,7 +447,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=インストールされていません
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=modpackを導入
|
||||
install.new_game=新規作成
|
||||
@@ -505,7 +504,7 @@ launcher.agreement.hint=このソフトウェアを使用するには、EULAに
|
||||
launcher.background=背景画像
|
||||
launcher.background.classic=クラシック
|
||||
launcher.background.choose=背景画像ファイルを選択してください
|
||||
launcher.background.default=標準(ランチャーと同じディレクトリにある background.png/jpg と bg フォルダから自動的に画像を取得します。)
|
||||
launcher.background.default=標準(ランチャーと同じディレクトリにある background.png/jpg/gif と bg フォルダから自動的に画像を取得します。)
|
||||
launcher.background.network=ネットワーク
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=キャッシュ用のディレクトリ
|
||||
@@ -668,7 +667,6 @@ multiplayer.exit.after_ready=マルチプレイヤーセッションが壊れて
|
||||
multiplayer.exit.before_ready=マルチプレイヤーセッションの作成に失敗しました。 cato exitcode %d
|
||||
multiplayer.exit.timeout=マルチプレイヤーサーバーへの接続に失敗しました。
|
||||
multiplayer.exit.timeout.dynamic_token=マルチプレイヤーサーバーへの接続に失敗しました。
|
||||
multiplayer.hint=マルチプレイヤー機能は実験的なものです。フィードバックをお願いします。
|
||||
multiplayer.nat=ネットワークタイプの検出
|
||||
multiplayer.nat.failed=ネットワークタイプの検出に失敗しました。
|
||||
multiplayer.nat.hint=ネットワークタイプの検出により、ネットワークがマルチプレイヤーモードの要件を満たしているかどうかが明確になります。
|
||||
@@ -685,7 +683,7 @@ 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://mcer.cn">mcer.cn</a>によって提供されます。 <a href\="https\://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement">Licence/a>
|
||||
multiplayer.powered_by=マルチプレイヤーサービスは<a href="https://mcer.cn">mcer.cn</a>によって提供されます。 <a href\="https://docs.hmcl.net/multiplayer/#%E4%BD%BF%E7%94%A8%E6%8E%88%E6%9D%83">Licence/a>
|
||||
multiplayer.report=レポート
|
||||
multiplayer.relay=リレーモード
|
||||
multiplayer.relay.hint=静的トークンを持ち、テストされたNATタイプが不良対称であるユーザーの場合
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
# Соавторы: vanja-san
|
||||
about=О лаунчере
|
||||
about.copyright=Авторские права
|
||||
about.copyright.statement=Авторские права (С) 2021 huangyuhui.
|
||||
about.copyright.statement=Авторские права © 2022 huangyuhui.
|
||||
about.author=Автор
|
||||
about.author.statement=Bilibili: @huanghongxun
|
||||
about.author.statement=bilibili: @huanghongxun
|
||||
about.claim=EULA
|
||||
about.claim.statement=Кликните на ссылку, чтобы увидеть весь текст.
|
||||
about.community=Сообщество
|
||||
@@ -41,8 +41,6 @@ about.thanks_to.mcbbs=MCBBS
|
||||
about.thanks_to.mcbbs.statement=MCBBS - поставщик источника скачивания
|
||||
about.thanks_to.mcmod=mcmod.cn
|
||||
about.thanks_to.mcmod.statement=Модифицированный перевод китайских имён
|
||||
about.thanks_to.noin=mcer.cn
|
||||
about.thanks_to.noin.statement=Поставщик многопользовательских услуг
|
||||
about.thanks_to.red_lnn.statement=Поставщику фонового изображения по умолчанию
|
||||
about.thanks_to.users=Группе участников
|
||||
about.thanks_to.users.statement=Благодаря пожертвованиям, сообщениям об ошибках и так далее
|
||||
@@ -106,6 +104,7 @@ account.methods.microsoft.makegameidsettings=Создать архив
|
||||
account.methods.microsoft.hint=Вам необходимо нажать кнопку «Войти» и продолжить процесс авторизации во вновь открывшемся окне браузера.\nЕсли токен, используемый для входа в аккаунт Microsoft случайно утёк, вы можете нажать «Отменить авторизацию аккаунта» ниже, чтобы отменить авторизацию.
|
||||
account.methods.microsoft.manual=После нажатия кнопки «Войти», вы должны завершить авторизацию во вновь открывшемся окне браузера. Если окно браузера не открылось, вы можете щёлкнуть здесь, чтобы скопировать ссылку и вручную открыть её в браузере.\nЕсли токен, используемый для входа в аккаунт Microsoft случайно утёк, вы можете нажать «Отменить авторизацию аккаунта» ниже, чтобы отменить авторизацию.
|
||||
account.methods.microsoft.profile=Профиль аккаунта...
|
||||
account.methods.forgot_password=ЗАБЫЛ ПАРОЛЬ
|
||||
account.methods.microsoft.snapshot=Лаунчер версии снапшот не поддерживает авторизацию через систему Microsoft.
|
||||
account.methods.microsoft.waiting_browser=Ожидание авторизации в открытом окне браузера...
|
||||
account.methods.offline=Офлайн
|
||||
@@ -322,7 +321,7 @@ fatal.config_loading_failure=Конфигурация недоступна.\nП
|
||||
fatal.migration_requires_manual_reboot=Обновление завершено. Перезапустите лаунчер.
|
||||
fatal.apply_update_failure=Мы сожалеем, лаунчер не смог завершить обновление, потому что что-то пошло не так.\nНо вы все ещё можете вручную завершить обновление, загрузив Hлаунчер с %s.\nВы можете сообщить нам об этой проблеме.
|
||||
fatal.samba=Если вы пытаетесь запустить лаунчер в общей папке Samba, он может не работать, попробуйте обновить Java или запустить лаунчер из локальной папки.
|
||||
fatal.illegal_char=Недопустимый символ '=' в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\nВы не сможете использовать аккаунт authlib-injector или изменить офлайн скин. Также в сетевой игре могут возникнуть большие риски для безопасности.
|
||||
fatal.illegal_char=Недопустимый символ '=' в пути к папке пользователя. Лаунчер может работать, но некоторые функции не будут работать.\nВы не сможете использовать аккаунт authlib-injector или изменить офлайн скин.
|
||||
|
||||
feedback=Обратная связь
|
||||
feedback.add=Добавить отзыв (на английском или китайском)
|
||||
@@ -450,7 +449,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=Не установлен
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=Установить модпак
|
||||
install.new_game=Установить новую игру
|
||||
@@ -672,8 +671,7 @@ multiplayer.exit.after_ready=Сессия сетевой игры сломана
|
||||
multiplayer.exit.before_ready=Не удалось создать сессию сетевой игры. Код выхода като %d
|
||||
multiplayer.exit.timeout.static_token=Не удалось подключиться к серверу сетевой игры. Переключитесь на динамический токен и попробуйте снова.
|
||||
multiplayer.exit.timeout.dynamic_token=Не удалось подключиться к серверу сетевой игры.
|
||||
multiplayer.hint=Функция сетевой игры экспериментальная. Оставьте свой отзыв.
|
||||
multiplayer.powered_by=Сетевая игра предоставлена <a href="https://mcer.cn">mcer.cn</a>. <a href\="https\://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement">Licence</a>
|
||||
multiplayer.powered_by=Сетевая игра предоставлена <a href="https://mcer.cn">mcer.cn</a>. <a href\="https://docs.hmcl.net/multiplayer/#%E4%BD%BF%E7%94%A8%E6%8E%88%E6%9D%83">Licence</a>
|
||||
multiplayer.report=Отчёт
|
||||
multiplayer.relay=Режим реле
|
||||
multiplayer.relay.hint=Для пользователей, у которых есть статический токен, и чей проверенный тип NAT - плохо-семитричный
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
# Contributors: pan93412
|
||||
about=關於
|
||||
about.copyright=著作權
|
||||
about.copyright.statement=著作權所有 (c) 2021 huangyuhui。
|
||||
about.copyright.statement=著作權所有 © 2022 huangyuhui。
|
||||
about.author=作者
|
||||
about.author.statement=Bilibili: @huanghongxun
|
||||
about.author.statement=bilibili: @huanghongxun
|
||||
about.claim=用戶協議
|
||||
about.claim.statement=點擊連結以查看全文
|
||||
about.community=社區
|
||||
@@ -41,8 +41,6 @@ about.thanks_to.mcbbs=MCBBS 我的世界中文論壇
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下載地點
|
||||
about.thanks_to.mcmod=MC 百科
|
||||
about.thanks_to.mcmod.statement=提供 Mod 中文名映射表與 Mod 百科
|
||||
about.thanks_to.noin=阿貓 (mcer.cn)
|
||||
about.thanks_to.noin.statement=提供多人聯機服務 (cato - ioi 系列作品)
|
||||
about.thanks_to.red_lnn.statement=提供預設背景圖
|
||||
about.thanks_to.users=HMCL 用戶群成員
|
||||
about.thanks_to.users.statement=感謝用戶群成員贊助充電、積極催更、迴響問題、出謀劃策
|
||||
@@ -60,7 +58,7 @@ account.create.yggdrasil=添加 Mojang 帳戶
|
||||
account.create.offline=添加離線模式帳戶
|
||||
account.create.authlibInjector=添加外置登入帳戶 (authlib-injector)
|
||||
account.email=電子信箱
|
||||
account.failed=登錄失敗
|
||||
account.failed=帳戶刷新失敗!
|
||||
account.failed.character_deleted=已刪除此角色
|
||||
account.failed.connect_authentication_server=無法連線至認證伺服器,可能是網路問題,請檢查設備能否正常上網或使用代理服務
|
||||
account.failed.connect_injector_server=無法連線至認證伺服器,可能是網路故障,請檢查設備能否正常上網、檢查網址是否輸入錯誤或使用代理服務
|
||||
@@ -70,7 +68,7 @@ account.failed.invalid_password=密碼無效
|
||||
account.failed.invalid_token=請嘗試登出並重新輸入密碼登入
|
||||
account.failed.migration=你的帳號需要被遷移至微軟帳號。如果你已經遷移,你需要使用微軟登錄方式登錄遷移後的微軟帳號。
|
||||
account.failed.no_character=該帳戶沒有角色
|
||||
account.failed.server_disconnected=無法訪問登錄伺服器。是否以離線模式繼續登錄(進入遊戲後無法進入有正版驗證的伺服器)?或嘗試重新登入?
|
||||
account.failed.server_disconnected=無法訪問登入服務器,帳戶信息刷新失敗!\n你可以選擇跳過帳戶刷新(但啓動遊戲後可能會因為帳戶信息過期而導致無法進入開啟正版驗證的服務器)\n或嘗試再次刷新帳戶?
|
||||
account.failed.server_response_malformed=無法解析認證伺服器回應,可能是伺服器故障
|
||||
account.failed.wrong_account=登錄了錯誤的帳號
|
||||
account.hmcl.hint=你需要點擊“登入”按鈕,並在打開的網頁中完成登入
|
||||
@@ -84,7 +82,8 @@ account.injector.server_url=伺服器位址
|
||||
account.injector.server_name=伺服器名稱
|
||||
account.login=登入
|
||||
account.login.hint=我們不會保存你的密碼
|
||||
account.login.offline=以離線模式登錄
|
||||
account.login.skip=重新刷新帳戶
|
||||
account.login.retry=再次刷新帳戶
|
||||
account.login.refresh=重新登錄
|
||||
account.login.refresh.microsoft.hint=<b>因為賬戶授權失效,需要重新添加你的微軟賬戶</b>\n你需要按照以下步驟添加:\n \n1.點擊“確定”按鈕\n2.在網頁瀏覽器顯示的網站中輸入 HMCL 顯示的代碼(自動拷貝,直接粘貼即可),並點擊“下一步”\n3.按照網站的提示登入\n4.當網站顯示“ 是否允許此應用訪問你的信息?”的標識時,請點擊“是”\n5.當網站提示“大功告成”的標示時,及表示登入成功!然後等待賬戶完成添加即可!\n \n-若網站提示“出现错误”的標識,請嘗試重新按照以上步驟登入\n-若賬戶添加失敗,請嘗試重新按照以上步驟登入\n<b>-若設備網絡環境不佳,可能登陸網站加載很慢甚至無法加載,此時請使用 VPN 或代理伺服器並重試</b>
|
||||
account.login.refresh.microsoft.manual=<b>因為賬戶授權失效,需要重新添加你的微軟賬戶</b>\n你需要按照以下步驟添加:\n \n1.點擊“確定”按鈕\n2.在網頁瀏覽器顯示的網站中輸入該代碼:<b>%1$s</b>(已自動拷貝,請點此再次拷貝),並點擊“下一步”\n3.按照網站的提示登入\n4.當網站顯示“ 是否允許此應用訪問你的信息?”的標識時,請點擊“是”\n5.當網站提示“大功告成”的標示時,及表示登入成功!然後等待賬戶完成添加即可!\n \n-若網站提示“出现错误”的標識,請嘗試重新按照以上步驟登入\n-若賬戶添加失敗,請嘗試重新按照以上步驟登入\n-若網站未能打開,請手動在網頁瀏覽器中打開:%2$s\n<b>-若設備網絡環境不佳,可能登陸網站加載很慢甚至無法加載,此時請使用 VPN 或代理伺服器並重試</b>
|
||||
@@ -109,9 +108,11 @@ account.methods.microsoft.makegameidsettings=創建檔案
|
||||
account.methods.microsoft.hint=你需要按照以下步驟添加:\n1.點擊“登入”按鈕\n2.在網頁瀏覽器顯示的網站中輸入 HMCL 顯示的代碼(自動拷貝,直接粘貼即可),並點擊“下一步”\n3.按照網站的提示登入\n4.當網站顯示“ 是否允許此應用訪問你的信息?”的標識時,請點擊“是”\n5.當網站提示“大功告成”的標示時,及表示登入成功!然後等待賬戶完成添加即可!\n-若網站提示“出现错误”的標識,請嘗試重新按照以上步驟登入\n-若賬戶添加失敗,請嘗試重新按照以上步驟登入\n<b>-若設備網絡環境不佳,可能登陸網站加載很慢甚至無法加載,此時請使用 VPN 或代理伺服器並重試</b>\n-若登入微軟賬號的 Token 洩露,可點擊下方“解除帳戶授權”解除登入授權,然後等待 Token 過期
|
||||
account.methods.microsoft.manual=你需要按照以下步驟添加:\n1.點擊“登入”按鈕\n2.在網頁瀏覽器顯示的網站中輸入該代碼:<b>%1$s</b>(已自動拷貝,請點此再次拷貝),並點擊“下一步”\n3.按照網站的提示登入\n4.當網站顯示“ 是否允許此應用訪問你的信息?”的標識時,請點擊“是”\n5.當網站提示“大功告成”的標示時,及表示登入成功!然後等待賬戶完成添加即可\n-若網站提示“出现错误”的標識,請嘗試重新按照以上步驟登入\n-若賬戶添加失敗,請嘗試重新按照以上步驟登入\n-若網站未能打開,請手動在網頁瀏覽器中打開:%2$s\n<b>-若設備網絡環境不佳,可能登陸網站加載很慢甚至無法加載,此時請使用 VPN 或代理伺服器並重試</b>\n-若登入微軟賬號的 Token 洩露,可點擊下方“解除帳戶授權”解除登入授權,然後等待 Token 過期
|
||||
account.methods.microsoft.profile=帳戶設置頁
|
||||
account.methods.forgot_password=忘記密碼
|
||||
account.methods.microsoft.snapshot=HMCL 快照版暫不提供微軟登入支持,請使用穩定版或測試版進行登入。
|
||||
account.methods.microsoft.waiting_browser=等待在新打開的瀏覽器窗口中完成登入...
|
||||
account.methods.offline=離線模式
|
||||
account.methods.offline.name.special_characters=建議使用英文字符、數字以及下劃線命名,否則可能導致模組異常或無法加入服務器
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUID 是 Minecraft 對玩家角色的唯一標識符,每個啟動器生成 UUID 的方式可能不同。通過修改 UUID 選項至原啟動器所生成的 UUID,你可以保證在切換啟動器後,遊戲還能將你的遊戲角色識別為給定 UUID 所對應的角色,從而保留原來角色的背包物品。UUID 選項為高級選項,除非你知道你在做什麼,否則你不需要調整該選項。
|
||||
account.methods.offline.uuid.malformed=格式錯誤
|
||||
@@ -153,6 +154,7 @@ assets.index.malformed=資源檔案的索引檔案損壞,您可以在遊戲 [
|
||||
button.cancel=取消
|
||||
button.change_source=切換下載源
|
||||
button.clear=清除
|
||||
button.copy_and_exit=複製並退出
|
||||
button.delete=刪除
|
||||
button.edit=編輯
|
||||
button.install=安裝
|
||||
@@ -218,6 +220,7 @@ curse.category.4486=幸運方塊 (Lucky Blocks)
|
||||
curse.category.432=建築 (Buildcraft)
|
||||
curse.category.418=基因
|
||||
curse.category.4671=Twitch
|
||||
curse.category.5314=KubeJS
|
||||
curse.category.408=礦物資源
|
||||
curse.category.4773=CraftTweaker
|
||||
curse.category.430=神秘 (Thaumcraft)
|
||||
@@ -294,6 +297,7 @@ download=下載
|
||||
download.code.404=遠端伺服器沒有需要下載的檔案: %s
|
||||
download.content=遊戲內容
|
||||
download.curseforge.customization=光影與遊戲定制
|
||||
download.curseforge.unavailable=HMCL 預覽版暫不支持訪問 CurseForge,請使用穩定版或測試版進行下載。
|
||||
download.existing=檔案已存在,無法保存。你可以選擇另存為將檔案保存至其他地方。
|
||||
download.external_link=打開下載網站
|
||||
download.failed=下載失敗: %1$s,錯誤碼:%2$d
|
||||
@@ -328,11 +332,13 @@ fatal.javafx.incompatible=缺少 JavaFX 運行環境。\nHMCL 無法在低於 Ja
|
||||
fatal.javafx.incompatible.loongson=缺少 JavaFX 運行環境。\n請使龍芯 JDK 8 (http://www.loongnix.cn/zh/api/java/downloads-jdk8/index.html) 啟動 HMCL。
|
||||
fatal.javafx.missing=找不到 JavaFX。\n如果您使用的是 Java 11 或更高版本,請降級到 Oracle JRE 8,或者安裝 BellSoft Liberica Full JRE。\n如果您使用的是其他 OpenJDK 构建,請確保其包含 OpenJFX。
|
||||
fatal.missing_dst_root_ca_x3=目前的 Java 平台缺少 DST Root CA X3 憑證。\n您依然可以使用 Hello Minecraft! Launcher,但會無法連線到部分網站 (如使用 Lets Encrypt 憑證的站台),這可能會使 Hello Minecraft! Launcher 無法正常運作。\n請將您的 Java 升級到 8u101 以上來解決此問題。
|
||||
fatal.config_change_owner_root=你正在使用 root 帳戶啟動 Hello Minecraft! Launcher,這可能導致你未來無法使用其他帳戶正常啟動 Hello Minecraft! Launcher。\n是否繼續啟動?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 無法載入設定檔案。\n請確保 Hello Minecraft! Launcher 對 "%s" 目錄及該目錄下的檔案擁有讀寫權限。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 無法載入設定檔案,因為設定檔案是由用戶 %1$s 創建的。\n請使用 root 帳戶啟動 HMCL (不推薦),或在終端中執行以下命令將設定檔案的所有權變更為當前用戶: \n%2$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。
|
||||
fatal.illegal_char=您的用戶資料夾路徑中存在非法字元‘=’。HMCL 能够運行,但一些功能無法正常使用。 \n您將無法使用外置登入帳戶以及離線登入更換皮膚功能,並且在進行多人遊戲時可能存在更大的安全隱患。
|
||||
fatal.illegal_char=由於您的用戶資料夾路徑中存在非法字元‘=’,您將無法使用外置登入帳戶以及離線登入更換皮膚功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未你您的平臺提供完善支持,所以可能影響遊戲體驗或無法啟動遊戲。\n若無法啟動 Minecraft 1.17 及以上版本,可以嘗試在版本設定中打開“使用 OpenGL 軟渲染器”選項,使用 CPU 渲染以獲得更好的相容性。
|
||||
fatal.unsupported_platform.osx_arm64=HMCL 已為 Apple Silicon 平臺提供支援,使用 ARM 原生 Java 啟動遊戲以獲得更流暢的遊戲體驗。\n如果你在遊戲中遭遇問題,使用 x86-64 架構的 Java 啟動遊戲可能有更好的相容性。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已為 Windows on Arm 平臺提供原生支持。如果你在遊戲中遭遇問題,請嘗試使用 x86 架構的 Java 啟動遊戲。\n\n如果你正在使用<b>高通</b>平臺,你可能需要安裝 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 相容包</a>後才能進行遊戲。點擊連結前往 Microsoft Store 安裝相容包。
|
||||
@@ -399,6 +405,7 @@ game.crash.reason.jdk_9=當前遊戲因為 Java 版本過高,無法繼續運
|
||||
game.crash.reason.jvm_32bit=當前遊戲因為記憶體分配過大,超過了 32 位 Java 記憶體限制,無法繼續運行。\n如果你的電腦是 64 位系統,請下載安裝並更換 64 位 Java。如果你的電腦室 32 位系統,你或許可以重新安裝 64 位系統,或換一台新電腦。\n或者,你可以關閉遊戲記憶體的自動分配,並且把記憶體限制調節為 1024 MB 或以下。
|
||||
game.crash.reason.loading_crashed_forge=當前遊戲因為模組 %1$s (%2$s) 錯誤,無法繼續運行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.loading_crashed_fabric=當前遊戲因為模組 %1$s 錯誤,無法繼續運行。\n你可以嘗試刪除或更新該模組以解決問題。
|
||||
game.crash.reason.mac_jdk_8u261=當前遊戲因為你所使用的 Forge 或 OptiFine 與 Java 衝突崩潰。\n請嘗試更新 Forge 和 OptiFine,或使用 Java 8u251 及更早版本啟動。
|
||||
game.crash.reason.memory_exceeded=當前遊戲因為分配的記憶體過大,無法繼續運行。\n該問題是由於系統頁面文件太小導致的。\n你需要在遊戲設置中關閉遊戲記憶體的自動分配,並將遊戲記憶體調低至遊戲能正常啟動為止。\n你還可以嘗試調大系統的頁面大小。
|
||||
game.crash.reason.mod=當前遊戲因為 %1$s 的問題,無法繼續運行。\n你可以更新或刪除已經安裝的 %1$s 再試。
|
||||
game.crash.reason.mod_resolution=當前遊戲因為 Mod 依賴問題,無法繼續運行。Fabric 提供了如下訊息:\n%1$s
|
||||
@@ -408,12 +415,17 @@ game.crash.reason.mod_resolution_missing=當前遊戲因為缺少 Mod 前置,
|
||||
game.crash.reason.mod_resolution_missing_minecraft=當前遊戲因為 Mod 和 Minecraft 遊戲版本不匹配,無法繼續運行。\n%1$s 需要 Minecraft %2$s 才能運行。\n如果你要繼續使用你已經安裝的 Mod,你可以選擇安裝對應的 Minecraft 版本;如果你要繼續使用當前 Minecraft 版本,你需要安裝對應版本的 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.optifine_is_not_compatible_with_forge=當前遊戲因為OptiFine與當前版本的Forge不相容,導致了遊戲崩潰。\n請前往 <a href="https://optifine.net/downloads">OptiFine 官網</a>查看 OptiFine 所相容的 Forge 版本,並嚴格按照對應版本重新安裝遊戲或在版本設定-自動安裝中更換版本。\n經測試,Forge版本過高或過低都可能導致崩潰。
|
||||
game.crash.reason.mod_files_are_decompressed=當前遊戲因為Mod檔案被解壓了,無法繼續運行。 \n請直接把整個Mod檔案放進Mod資料夾中即可! \n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=當前遊戲因為您所安裝的Mod過多,超出了遊戲的ID限制,無法繼續運行。 \n請嘗試安裝<a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a>等修復Mod,或删除部分大型Mod。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=當前遊戲因為Mod檔案被解壓了,無法繼續運行。\n請直接把整個Mod檔案放進Mod資料夾中即可!\n若解壓就會導致遊戲出錯,請删除Mod資料夾中已被解壓的Mod,然後再啟動遊戲。
|
||||
game.crash.reason.modlauncher_8=當前遊戲因為您所使用的 Forge 版本與當前使用的 Java 衝突崩潰。請嘗試更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本啟動。
|
||||
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你可能需要重新安裝遊戲和 Mod,或請求他人幫助。
|
||||
game.crash.reason.opengl_not_supported=當前遊戲因為你的顯示卡驅動存在問題,無法繼續運行。\n原因是 OpenGL 不受支援,你現在是否在遠程桌面或者串流模式下?如果是,請直接使用原電腦啟動遊戲。\n或者嘗試升級你的顯示卡驅動到最新版本後再嘗試啟動遊戲。如果你的電腦存在獨立顯示卡,你需要檢查遊戲是否使用集成/核心顯示卡啟動,如果是,請嘗試使用獨立顯示卡啟動 HMCL 與遊戲。如果仍有問題,你可能需要考慮換一個新顯示卡或新電腦。
|
||||
game.crash.reason.openj9=當前遊戲無法運行在 OpenJ9 虛擬機上,請你在遊戲設置中更換 Hotspot Java 虛擬機,並重新啟動遊戲。如果沒有下載安裝,你可以在網路上自行下載。
|
||||
game.crash.reason.out_of_memory=當前遊戲因為記憶體不足,無法繼續運行。\n這可能是記憶體分配太小,或者 Mod 數量過多導致的。\n你可以在遊戲設置中調大遊戲記憶體分配值以允許遊戲在更大的記憶體下運行。\n如果仍然出現該錯誤,你可能需要換一台更好的電腦。
|
||||
game.crash.reason.processing_of_javaagent_failed=當前遊戲因為加載 -javaagent 參數失敗,無法繼續運行。\n如果你在 Java 虛擬機參數 中添加了相關參數,請檢查是否正確。\n如果你沒有添加相關參數或參數確認正確, 請嘗試:\n打開 控制面板 -- 時鐘和區域 分類(選項為類別顯示才有此選項,沒有就跳過)-- 區域 -- 上方的 管理 選項卡 -- 下方的 更改系統區域設置 按鈕 -- 在彈出的視窗中將 “使用Unicode UTF-8提供全球語言支持” 選項關閉,重啓設備後再嘗試啟動遊戲。\n可在<a href="https://discord.gg/jVvC7HfM6U">Discord</a>或<a href="https://kook.top/Kx7n3t">KOOK群</a>尋求幫助
|
||||
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 虛擬機,並重新啟動遊戲。如果沒有下載安裝,你可以點擊 <a href="https://docs.microsoft.com/zh-TW/java/openjdk/download">此處</a> 下載微軟 JDK。
|
||||
@@ -463,7 +475,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=不安裝
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=安裝模組包
|
||||
install.new_game=安裝新遊戲版本
|
||||
@@ -522,7 +534,7 @@ launcher.agreement.hint=同意本軟體的用戶協議與免責聲明以使用
|
||||
launcher.background=背景位址
|
||||
launcher.background.choose=選擇背景路徑
|
||||
launcher.background.classic=經典
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg 及 bg 資料夾內的圖片)
|
||||
launcher.background.default=預設(自動尋找啟動器同目錄下的 background.png/jpg/gif 及 bg 資料夾內的圖片)
|
||||
launcher.background.network=網路
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=檔案下載快取目錄
|
||||
@@ -633,28 +645,43 @@ modpack.wizard.step.initialization.warning=在製作模組包前,請您確認
|
||||
modpack.wizard.step.initialization.server=點選此處查看有關伺服器自動更新模組包的製作教學
|
||||
|
||||
modrinth.category.adventure=冒險
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=高難度
|
||||
modrinth.category.combat=戰鬥
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=裝飾
|
||||
modrinth.category.economy=經濟
|
||||
modrinth.category.equipment=裝備
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.food=食物
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=遊戲機制
|
||||
modrinth.category.kitchen-sink=大雜燴
|
||||
modrinth.category.library=支持庫
|
||||
modrinth.category.lightweight=輕量
|
||||
modrinth.category.magic=魔法
|
||||
modrinth.category.management=管理
|
||||
modrinth.category.minigame=小遊戲
|
||||
modrinth.category.misc=其他
|
||||
modrinth.category.mobs=生物
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=多人
|
||||
modrinth.category.optimization=最佳化
|
||||
modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=任務
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.social=社交
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=儲存
|
||||
modrinth.category.technology=科技
|
||||
modrinth.category.transportation=運輸
|
||||
modrinth.category.utility=實用
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
|
||||
mods=模組
|
||||
@@ -693,38 +720,61 @@ multiplayer.download=正在下載相依元件
|
||||
multiplayer.download.failed=初始化失敗,部分檔案未能完成下載
|
||||
multiplayer.download.success=多人聯機初始化完成
|
||||
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台
|
||||
multiplayer.error.failed_get_device=HiPer 無法創建網絡設備,可能是HiPer已經啟動或缺少管理員權限。
|
||||
multiplayer.error.failed_get_device=HiPer無法創建網路設備,可能是 HiPer 正在運行中。請關閉正在運行中的其他啟動器,或通過工作管理員終止 HiPer。
|
||||
multiplayer.error.failed_load_config=HiPer 初始化失敗,可能是配置文件存在問題或證書已失效。
|
||||
multiplayer.error.file_not_found=找不到 HiPer 程式。該程式應該在進入多人聯機頁面時完成下載。請重啟 HMCL 再試。\n請檢查你電腦的防毒軟體是否將 HiPer 標記為病毒,如果是,請恢復 HiPer。
|
||||
multiplayer.error.session_expired=本次使用時間已結束,若要繼續使用,請更新憑證
|
||||
multiplayer.error.failed_sudo.linux=HiPer 需要管理員許可權才能運行,您可以通過設置環境變量 HMCL_ASKPASS 配置 HiPer 如何申請權限。\n若將 HMCL_ASKPASS 設置為 'false',請通過配置 sudoers 文件允許 '%s' 免密獲取 root 權限,重啟後再次啟動 HMCL 即可;\n若將 HMCL_ASKPASS 設置為 'user',請先設置環境變量 SUDO_ASKPASS,HMCL 將使用 sudo --askpass 啟動 HiPer (具體請參見 man sudo)。
|
||||
multiplayer.error.failed_sudo.mac=HiPer 需要管理員許可權才能運行。是否要授予 HiPer 管理員許可權? \n點擊“是”授予 HiPer 管理員許可權,然後重啓電腦後再次打開啟動器即可聯機。
|
||||
multiplayer.error.failed_sudo.windows=HiPer 需要管理員權限才能運行,請使用管理員權限重新啟動 HMCL。是否查看教程?
|
||||
multiplayer.error.file_not_found=已發現 HiPer 有可用的更新,請重新進入多人聯機頁面以更新 HiPer。
|
||||
multiplayer.error.session_expired=本次使用時間已結束,若要繼續使用,請更新索引碼
|
||||
multiplayer.exit=HiPer 意外退出,退出碼 %d
|
||||
multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請前往 mcer.cn 回饋
|
||||
multiplayer.powered_by=本服務由 (<a href="https://mcer.cn">速聚</a>) 提供。<a href\="https\://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement">授權協議</a>
|
||||
multiplayer.help.1=管理員權限打開
|
||||
multiplayer.help.2=多人聯機教程
|
||||
multiplayer.help.3=創建方幫助
|
||||
multiplayer.help.4=參與者幫助
|
||||
multiplayer.help.text=測試能否聯機
|
||||
multiplayer.hint=多人聯機服務正在維護中。
|
||||
multiplayer.hint.details=查看詳情
|
||||
multiplayer.persistence=授權信息管理
|
||||
multiplayer.persistence.export=將授權信息導出至文件
|
||||
multiplayer.persistence.export.button=導出授權文件
|
||||
multiplayer.persistence.export.failed=導出授權文件失敗,請檢查您所選擇的保存路徑。
|
||||
multiplayer.persistence.export.file_not_exists=獲取授權文件失敗,請檢查索引碼是否輸入正確。
|
||||
multiplayer.persistence.export.title=選擇授權文件保存路徑
|
||||
multiplayer.persistence.hint=此功能為長期授權 (有效期超過 24 小時) 用戶提供,如果你正在使用短期授權 (有效期不超過 24 小時),則無需使用本功能。\n當你使用長期授權時,請在獲取授權後的 24 小時內使用索引碼啟動 HiPer,否則 HiPer 將無法獲取授權信息。\n隨後,你可以通過下方的“導出授權信息”功能保存授權文件。\n請注意妥善保管授權文件,在獲取授權 24 小時後,該文件是你的長期授權的唯一憑證,丟失該文件將無法通過索引碼進行聯機。
|
||||
multiplayer.persistence.import=從文件導入授權信息
|
||||
multiplayer.persistence.import.button=導入授權文件
|
||||
multiplayer.persistence.import.failed=導入授權文件失敗。
|
||||
multiplayer.persistence.import.file_already_exists=授權文件已經存在。
|
||||
multiplayer.persistence.import.title=選擇授權文件
|
||||
multiplayer.persistence.import.token_not_match=你選擇的授權文件可能與索引碼不匹配,是否繼續導入?
|
||||
multiplayer.persistence.license_file=授權文件
|
||||
multiplayer.powered_by=本服務由 (<a href="https://mcer.cn">速聚</a>) 提供。<a href\="https://docs.hmcl.net/multiplayer/#%E4%BD%BF%E7%94%A8%E6%8E%88%E6%9D%83">授權協議</a>
|
||||
multiplayer.report=違法違規檢舉
|
||||
multiplayer.token=憑證
|
||||
multiplayer.token.apply=申請憑證
|
||||
multiplayer.token.invalid=憑證不正確。
|
||||
multiplayer.token=索引碼
|
||||
multiplayer.token.apply=申請索引碼
|
||||
multiplayer.token.format_invalid=索引碼格式不正確
|
||||
multiplayer.token.invalid=索引碼不正確。
|
||||
multiplayer.token.expired=HiPer 證書過期,請重新啟動 HMCL 再試。
|
||||
multiplayer.token.malformed=HiPer 配置檔案無法解析,請重新啟動 HMCL 再試。
|
||||
multiplayer.token.prompt=你需要憑證才能使用多人聯機服務。點擊旁邊的“申請憑證”查看詳情
|
||||
multiplayer.off=啟動 HiPer
|
||||
multiplayer.off.hint=多人聯機服務由 HiPer 提供,你需要先使用管理員權限啟動 HMCL(具體操作方法詳見左側的 幫助),然後輸入憑證啟動 HiPer 以使用多人聯機服務。
|
||||
multiplayer.token.prompt=你需要索引碼才能使用多人聯機服務。點擊旁邊的“申請索引碼”查看詳情
|
||||
multiplayer.off.hint=由於需要注册系統網路服務許可權,啟動 HiPer 時將會向用戶申請管理員許可權。若出現認證提示,允許以啟動 HiPer。
|
||||
multiplayer.off.start=啟動 HiPer
|
||||
multiplayer.on=HiPer
|
||||
multiplayer.master=创建方提示
|
||||
multiplayer.master.hint=1.啟動遊戲\n2.選擇單人遊戲模式進入一個存檔\n3.在遊戲選項菜單內選擇 對局域網開放 選項,之後你可以在遊戲聊天框內看到遊戲提示的端口號(通常是 5 位數字)\n4.點擊下方的生成伺服器地址按鈕,輸入端口號,你就可以獲得你的伺服器地址了\n5.該地址需要提供給其他需要加入伺服器的玩家用於添加伺服器。
|
||||
multiplayer.master.hint=1.啟動遊戲\n2.選擇單人遊戲模式進入一個存檔\n3.在遊戲選項菜單內選擇 對局域網開放 選項,之後你可以在遊戲聊天框內看到遊戲提示的端口號(通常是 4~5 位數字)\n4.點擊下方的生成伺服器地址按鈕,輸入端口號,你就可以獲得你的伺服器地址了\n5.該地址需要提供給其他需要加入伺服器的玩家用於添加伺服器。
|
||||
multiplayer.master.server_address=生成伺服器地址
|
||||
multiplayer.master.server_address.copy=拷貝
|
||||
multiplayer.master.video_tutorial=教學影片
|
||||
multiplayer.master.port=埠號
|
||||
multiplayer.master.port.validate=在遊戲聊天框中出現的埠號 (0~65535)
|
||||
multiplayer.slave=參與者提示
|
||||
multiplayer.slave.hint=1.要求創建方按照上方的 創建方提示 操作\n2.啟動遊戲\n3.選擇多人遊戲模式,選擇添加伺服器\n4.遊戲會要求你輸入伺服器地址,你只需要向創建方索要伺服器地址並輸入,並進入伺服器即可。 \n- 注意:\n1.一般情況下,參與者的遊戲賬戶必須是 微軟賬戶 或 外置登錄賬戶(如 Little Skin),否則你需要將伺服器地址輸入至下方的輸入框中並點擊廣播,在遊戲中選擇多人遊戲模式,進入局域網世界方可加入,具體操作方法詳見左側的 幫助 \n2.一般情況下,參與者的遊戲版本、模組要必須與創建方的一致,否則加入失敗。
|
||||
multiplayer.slave.server_address=創建方服務器地址
|
||||
multiplayer.slave.hint=1.要求創建方按照上方的 創建方提示 操作\n2.啟動遊戲\n3.選擇多人遊戲模式,選擇添加服務器\n4.遊戲會要求你輸入服務器地址,你只需要向創建方索要服務器地址並輸入,並進入服務器即可。
|
||||
multiplayer.slave.hint2=1.一般情況下,參與者的遊戲賬戶必須是 微軟賬戶 或 外置登錄賬戶(如 Little Skin),否則你需要將服務器地址輸入至下方的輸入框中並點擊“加入”,在遊戲中選擇多人遊戲模式,進入局域網世界方可加入\n2.一般情況下,參與者的遊戲版本、模組要必須與創建方的一致,否則加入失敗。
|
||||
multiplayer.slave.server_address=創建方伺服器地址
|
||||
multiplayer.slave.server_address.start=加入
|
||||
multiplayer.slave.server_address.stop=退出
|
||||
multiplayer.slave.video_tutorial=教學影片
|
||||
multiplayer.session.expiration=本次使用截止時間
|
||||
multiplayer.session.expiration=索引碼到期時間
|
||||
|
||||
datapack=資料包
|
||||
datapack.add=加入資料包
|
||||
|
||||
@@ -19,14 +19,14 @@
|
||||
# Contributors: huangyuhui
|
||||
about=关于
|
||||
about.copyright=版权
|
||||
about.copyright.statement=版权所有 (c) 2021 huangyuhui.
|
||||
about.copyright.statement=版权所有 © 2022 huangyuhui.
|
||||
about.author=作者
|
||||
about.author.statement=B 站 ID: @huanghongxun
|
||||
about.author.statement=bilibili ID: @huanghongxun
|
||||
about.claim=用户协议
|
||||
about.claim.statement=点击链接以查看全文
|
||||
about.community=社区
|
||||
about.community.discord=Discord
|
||||
about.community.discord.statement=欢迎加入 Discord 讨论区,加入后请遵守讨论区规定
|
||||
about.community.discord.statement=欢迎加入 Discord 讨论区,加入后请遵守讨论区规定!
|
||||
about.community.kookapp=KOOK
|
||||
about.community.kookapp.statement=欢迎加入 KOOK 讨论区,加入后请遵守讨论区规定!
|
||||
about.dependency=依赖
|
||||
@@ -41,8 +41,6 @@ about.thanks_to.mcbbs=MCBBS 我的世界中文论坛
|
||||
about.thanks_to.mcbbs.statement=提供 MCBBS 下载源
|
||||
about.thanks_to.mcmod=MC 百科
|
||||
about.thanks_to.mcmod.statement=提供 Mod 中文名映射表与 Mod 百科
|
||||
about.thanks_to.noin=阿猫 (mcer.cn)
|
||||
about.thanks_to.noin.statement=提供多人联机服务 (cato - ioi 系列作品)
|
||||
about.thanks_to.red_lnn.statement=提供默认背景图
|
||||
about.thanks_to.users=HMCL 用户群成员
|
||||
about.thanks_to.users.statement=感谢用户群成员赞助充电、积极催更、反馈问题、出谋划策
|
||||
@@ -60,7 +58,7 @@ account.create.yggdrasil=添加 Mojang 帐户
|
||||
account.create.offline=添加离线模式帐户
|
||||
account.create.authlibInjector=添加外置登录帐户 (authlib-injector)
|
||||
account.email=邮箱
|
||||
account.failed=登录失败
|
||||
account.failed=账户刷新失败!
|
||||
account.failed.character_deleted=此角色已被删除
|
||||
account.failed.connect_authentication_server=无法连接认证服务器,可能是网络问题,请检查设备能否正常上网或使用代理服务
|
||||
account.failed.connect_injector_server=无法连接认证服务器,可能是网络问题,请检查设备是否能正常上网,检查 URL 是否输入错误,或使用代理服务
|
||||
@@ -70,7 +68,7 @@ account.failed.invalid_password=无效的密码
|
||||
account.failed.invalid_token=请尝试登出并重新输入密码登录
|
||||
account.failed.migration=你的帐号需要被迁移至微软帐号。如果你已经迁移,你需要使用微软登录方式登录迁移后的微软帐号。
|
||||
account.failed.no_character=该帐号没有角色
|
||||
account.failed.server_disconnected=无法访问登录服务器。是否以离线模式继续登录(进入游戏后无法进入有正版验证的服务器)?或尝试重新登录?
|
||||
account.failed.server_disconnected=无法访问登录服务器,账户信息刷新失败!\n你可以选择 跳过账户刷新(但启动游戏后可能会因为账户信息过期而导致无法进入开启正版验证的服务器)\n或尝试再次刷新账户?
|
||||
account.failed.server_response_malformed=无法解析认证服务器响应,可能是服务器故障
|
||||
account.failed.wrong_account=登录了错误的帐号
|
||||
account.hmcl.hint=你需要点击“登录”按钮,并在打开的网页中完成登录
|
||||
@@ -84,7 +82,8 @@ account.injector.server_url=服务器地址
|
||||
account.injector.server_name=服务器名称
|
||||
account.login=登录
|
||||
account.login.hint=我们不会保存你的密码
|
||||
account.login.offline=以离线模式登录
|
||||
account.login.skip=跳过账户刷新
|
||||
account.login.retry=再次刷新账户
|
||||
account.login.refresh=重新登录
|
||||
account.login.refresh.microsoft.hint=<b>因为账户授权失效,需要重新添加你的微软账户</b>\n你需要按照以下步骤添加:\n \n1.点击“确定”按钮\n2.在网页浏览器显示的网站中输入 HMCL 显示的代码(自动拷贝,直接粘贴即可),并点击“下一步”\n3.按照网站的提示登录\n4.当网站提示“是否允许此应用访问你的信息?”的标识时,请点击“是”\n5.当网站提示“大功告成”的标识时,及表示登录成功!然后等待账户完成添加即可!\n \n-若网站提示“出现错误”的标识时,请重新按照以上步骤重新添加\n-若账户添加失败,请重新按照以上步骤重新添加\n<b>-若设备网络环境不佳,可能登录网站加载很慢甚至无法加载,此时请使用 VPN 或代理服务器并重试</b>\n-若登录微软账户的 Token (不是密码)泄露,可点击下方“解除账户绑定”解除登录授权,然后等待 Token 过期
|
||||
account.login.refresh.microsoft.manual=<b>因为账户授权失效,需要重新添加你的微软账户</b>\n你需要按照以下步骤添加:\n \n1.点击“确定”按钮\n2.在网页浏览器显示的网站中输入:<b>%1$s</b>(已自动拷贝,点此再次拷贝),并点击“下一步”\n3.按照网站的提示登录\n4.当网站提示“是否允许此应用访问你的信息?”的标识时,请点击“是”\n5.当网站提示“大功告成”的标识时,及表示登录成功!然后等待账户完成添加即可!\n \n-若网站提示“出现错误”的标识时,请重新按照以上步骤重新添加\n-若账户添加失败,请重新按照以上步骤重新添加\n-若网站未能显示,请手动在网页浏览器中打开:%2$s\n<b>-若设备网络环境不佳,可能登录网站加载很慢甚至无法加载,此时请使用 VPN 或代理服务器并重试</b>\n-若登录微软账户的 Token (不是密码)泄露,可点击下方“解除账户绑定”解除登录授权,然后等待 Token 过期
|
||||
@@ -102,16 +101,18 @@ account.methods.microsoft.error.add_family=由于你未满 18 岁,你的帐户
|
||||
account.methods.microsoft.error.add_family_probably=请检查你的帐户设置,如果年龄未满 18 岁,你的帐户必须被加入到家庭中才能登录游戏。你也可以点击上方链接更改你的帐户的出生日期,使年龄满 18 岁以上以继续登录。
|
||||
account.methods.microsoft.error.country_unavailable=你所在的国家或地区不受 XBox Live 的支持。
|
||||
account.methods.microsoft.error.missing_xbox_account=你的微软帐户尚未关联 XBox 帐户,你必须先创建 XBox 帐户,才能登录游戏。
|
||||
account.methods.microsoft.error.no_character=该帐户未包含 Minecraft Java 版购买记录\n可能未创建游戏档案,请点击上方链接创建
|
||||
account.methods.microsoft.error.no_character=该帐户未包含 Minecraft Java 版购买记录\n若已购买,则可能未创建游戏档案,请点击上方链接创建。\n若确定该账号完成了上述步骤,请先在 Minecraft 官网(minecraft.net)登录一次账户,然后再在启动器登录。
|
||||
account.methods.microsoft.error.unknown=登录失败,错误码:%d
|
||||
account.methods.microsoft.logging_in=登录中……
|
||||
account.methods.microsoft.makegameidsettings=创建档案
|
||||
account.methods.microsoft.hint=你需要按照以下步骤添加:\n1.点击“登录”按钮\n2.在网页浏览器显示的网站中输入 HMCL 显示的代码(自动拷贝,直接粘贴即可),并点击“下一步”\n3.按照网站的提示登录\n4.当网站提示“是否允许此应用访问你的信息?”的标识时,请点击“是”\n5.当网站提示“大功告成”的标识时,及表示登录成功!然后等待账户完成添加即可!\n-若网站提示“出现错误”的标识时,请重新按照以上步骤重新添加\n-若账户添加失败,请重新按照以上步骤重新添加\n<b>-若设备网络环境不佳,可能登录网站加载很慢甚至无法加载,此时请使用 VPN 或代理服务器并重试</b>\n-若登录微软账户的 Token (不是密码)泄露,可点击下方“解除账户绑定”解除登录授权,然后等待 Token 过期
|
||||
account.methods.microsoft.manual=你需要按照以下步骤添加:\n1.点击“登录”按钮\n2.在网页浏览器显示的网站中输入:<b>%1$s</b>(已自动拷贝,点此再次拷贝),并点击“下一步”\n3.按照网站的提示登录\n4.当网站提示“是否允许此应用访问你的信息?”的标识时,请点击“是”\n5.当网站提示“大功告成”的标识时,及表示登录成功!然后等待账户完成添加即可\n-若网站提示“出现错误”的标识时,请重新按照以上步骤重新添加\n-若账户添加失败,请重新按照以上步骤重新添加\n-若网站未能显示,请手动在网页浏览器中打开:%2$s\n<b>-若设备网络环境不佳,可能登录网站加载很慢甚至无法加载,此时请使用 VPN 或代理服务器并重试</b>\n-若登录微软账户的 Token (不是密码)泄露,可点击下方“解除账户绑定”解除登录授权,然后等待 Token 过期
|
||||
account.methods.microsoft.profile=帐户设置页
|
||||
account.methods.microsoft.snapshot=HMCL 快照版暂不提供微软登录支持,请使用稳定版或测试版进行登录。
|
||||
account.methods.forgot_password=忘记密码
|
||||
account.methods.microsoft.snapshot=HMCL 快照版暂不提供微软登录支持,请使用推荐版本(稳定版)或测试版进行登录!
|
||||
account.methods.microsoft.waiting_browser=等待在新打开的浏览器窗口中完成登录……
|
||||
account.methods.offline=离线模式
|
||||
account.methods.offline.name.special_characters=建议使用英文字符、数字以及下划线命名,否则可能导致模组异常或无法加入服务器
|
||||
account.methods.offline.uuid=UUID
|
||||
account.methods.offline.uuid.hint=UUID 是 Minecraft 对玩家角色的唯一标识符,每个启动器生成 UUID 的方式可能不同。通过修改 UUID 选项至原启动器所生成的 UUID,你可以保证在切换启动器后,游戏还能将你的游戏角色识别为给定 UUID 所对应的角色,从而保留原来角色的背包物品。UUID 选项为高级选项,除非你知道你在做什么,否则你不需要调整该选项!
|
||||
account.methods.offline.uuid.malformed=格式错误
|
||||
@@ -153,6 +154,7 @@ assets.index.malformed=资源文件的索引文件损坏,您可以在相应的
|
||||
button.cancel=取消
|
||||
button.change_source=切换下载源
|
||||
button.clear=清除
|
||||
button.copy_and_exit=复制并退出
|
||||
button.delete=删除
|
||||
button.edit=修改
|
||||
button.install=安装
|
||||
@@ -218,6 +220,7 @@ curse.category.4486=幸运方块 (Lucky Blocks)
|
||||
curse.category.432=建筑 (Buildcraft)
|
||||
curse.category.418=基因
|
||||
curse.category.4671=Twitch
|
||||
curse.category.5314=KubeJS
|
||||
curse.category.408=矿物资源
|
||||
curse.category.4773=CraftTweaker
|
||||
curse.category.430=神秘 (Thaumcraft)
|
||||
@@ -294,6 +297,7 @@ download=下载
|
||||
download.code.404=远程服务器不包含需要下载的文件: %s
|
||||
download.content=游戏内容
|
||||
download.curseforge.customization=光影与游戏定制
|
||||
download.curseforge.unavailable=HMCL 预览版暂不支持访问 CurseForge,请使用稳定版或测试版进行下载。
|
||||
download.existing=文件已存在,无法保存。你可以在模组选择栏中的右侧按钮另存为将文件保存至其他地方。
|
||||
download.external_link=打开下载网站
|
||||
download.failed=下载失败: %1$s,错误码:%2$d
|
||||
@@ -328,11 +332,13 @@ fatal.javafx.incompatible=缺少 JavaFX 运行环境。\nHMCL 无法在低于 Ja
|
||||
fatal.javafx.incompatible.loongson=缺少 JavaFX 运行环境。\n请使用龙芯 JDK 8 (http://www.loongnix.cn/zh/api/java/downloads-jdk8/index.html) 启动 HMCL。
|
||||
fatal.javafx.missing=缺少 JavaFX 运行环境。\n如果您使用的是 Java 11 或更高版本,请降级到 Oracle JRE 8(java.com),或者安装 BellSoft Liberica Full JRE(bell-sw.com/pages/downloads/?package=jre-full)。\n如果您使用的是其他 OpenJDK 构建,请确保其包含 OpenJFX!
|
||||
fatal.missing_dst_root_ca_x3=当前 Java 平台缺少 DST Root CA X3 证书。\n您依然可以使用 Hello Minecraft! Launcher,但将无法连接到部分站点(如使用 Lets Encrypt 证书的站点),这可能会使 HMCL 无法正常工作。\n请将您的 Java 升级到 8u101 以上以解决此问题。
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限!
|
||||
fatal.config_change_owner_root=你正在使用 root 账户启动 Hello Minecraft! Launcher, 这可能导致你未来无法正常使用其他账户正常启动 Hello Minecraft! Launcher。\n是否继续启动?
|
||||
fatal.config_loading_failure=Hello Minecraft! Launcher 无法加载配置文件\n请确保 Hello Minecraft! Launcher 对 "%s" 目录及该目录下的文件拥有读写权限。
|
||||
fatal.config_loading_failure.unix=Hello Minecraft! Launcher 无法加载配置文件,因为配置文件是由用户 %1$s 创建的。\n请使用 root 账户启动 HMCL (不推荐),或在终端中执行以下命令将配置文件的所有权变更为当前用户:\n%2$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。
|
||||
fatal.illegal_char=您的用户文件夹路径中存在非法字符‘=’。HMCL 能够运行,但可能某些功能无法正常使用!\n你将无法使用外置登录账户以及离线登录更换皮肤功能,并且在进行多人游戏时可能存在更大的安全隐患!
|
||||
fatal.illegal_char=由于你的用户文件夹路径中存在非法字符‘=’,你将无法使用外置登录账户以及离线登录更换皮肤功能。
|
||||
fatal.unsupported_platform=Minecraft 尚未为您的平台提供完善支持,所以可能影响游戏体验或无法启动游戏。\n若无法启动 Minecraft 1.17 及以上版本,可以尝试在版本设置中打开“使用 OpenGL 软渲染器”选项,使用 CPU 渲染以获得更好的兼容性。
|
||||
fatal.unsupported_platform.osx_arm64=HMCL 已为 Apple Silicon 平台提供支持,使用 ARM 原生 Java 启动游戏以获得更流畅的游戏体验。\n如果你在游戏中遭遇问题,使用 x86-64 架构的 Java 启动游戏可能有更好的兼容性。
|
||||
fatal.unsupported_platform.windows_arm64=HMCL 已为 Windows on Arm 平台提供原生支持。如果你在游戏中遭遇问题,请尝试使用 x86 架构的 Java 启动游戏。\n\n如果你正在使用<b>高通</b>平台,你可能需要安装 <a href="ms-windows-store://pdp/?productid=9NQPSL29BFFF">OpenGL 兼容包</a>后才能进行游戏。点击链接前往 Microsoft Store 安装兼容包。
|
||||
@@ -383,23 +389,26 @@ game.crash.reason.bootstrap_failed=当前游戏因为模组 %1$s 错误,无法
|
||||
game.crash.reason.config=当前游戏因为无法解析模组配置文件,无法继续运行\n模组 %1$s 的配置文件 %2$s 无法被解析。
|
||||
game.crash.reason.debug_crash=当前游戏因为手动触发崩溃,无法继续运行。\n事实上游戏并没有问题,问题都是你造成的!
|
||||
game.crash.reason.duplicated_mod=当前游戏因为模组 %1$s 重复安装,无法继续运行。\n%2$s\n每种 Mod 只能安装一个,请你删除多余的 Mod 再试……
|
||||
game.crash.reason.entity=当前游戏因为某个实体不能正常工作,无法继续运行。\n你可以尝试通过 MCEdit 工具编辑存档删除该实体,或者直接删除相应的 Mod。\n实体类型:%1$s\n实体坐标:%2$s
|
||||
game.crash.reason.entity=当前游戏因为某个实体不能正常工作,无法继续运行。\n你可以尝试通过 <a href="https://podshot.github.io/MCEdit-Unified">MCEdit</a> 工具编辑存档删除该实体,或者直接删除相应的 Mod。\n实体类型:%1$s\n实体坐标:%2$s
|
||||
game.crash.reason.fabric_version_0_12=Fabric 0.12 及以上版本与当前已经安装的 Mod 可能不兼容,你需要将 Fabric 降级至 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=当前游戏因为文件校验失败,无法继续运行。\n如果你手动修改了 Minecraft.jar 文件,你需要回退修改,或者重新下载游戏。
|
||||
game.crash.reason.gl_operation_failure=当前游戏因为你使用的某些 Mod、光影包、材质包,无法继续运行。\n请先尝试禁用你所使用的Mod/光影包/材质包再试。
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了,请尝试升级你的显卡驱动到最新版本后再尝试启动游戏。\n\
|
||||
如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核芯显卡启动。如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑……\n\
|
||||
如果你确实需要使用核芯显卡,请检查你的电脑的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更旧的处理器,如果是,对于 Minecraft 1.16.5 及更旧版本,请你将游戏所使用的 Java 版本降级至 1.8.0_51 及以下版本 <a href="https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html">Java 1.8.0 历史版本</a> ,否则你需要更换独立显卡或新电脑。\n\
|
||||
在版本设置中打开“使用 OpenGL 软渲染器”选项也可以解决此问题,但打开此选项后在 CPU 性能不足的情况下帧数会显著降低,仅推荐以调试为目的或应急时开启。\n\
|
||||
game.crash.reason.graphics_driver=当前游戏因为你的显卡驱动存在问题崩溃了,请尝试:\n\
|
||||
- 如果你的电脑存在独立显卡,请尝试使用 独立显卡 而非 Intel 核显启动 HMCL 与游戏;\n\
|
||||
- <a href="https://support.microsoft.com/zh-cn/windows/ec62f46c-ff14-c91d-eead-d7126dc1f7b6">尝试升级你的 显卡驱动 到最新版本</a>,或回退到出厂版本;\n\
|
||||
- 如果你确实需要使用核芯显卡,请检查你的电脑的 CPU 是否是 Intel(R) Core(TM) 3000 系列或更旧的处理器,如果是,对于 Minecraft 1.16.5 及更旧版本,请你将游戏所使用的 Java 版本降级至 1.8.0_51 及以下版本 <a href="https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html">Java 1.8.0 历史版本</a> ,否则请跳过;\n\
|
||||
- 在全局(特定)游戏设置,菜单底部中打开“使用 OpenGL 软渲染器”选项可以尝试解决此问题,但打开此选项后在 CPU 性能不足的情况下帧数会显著降低,请慎重选择(仅推荐以调试为目的或应急时开启)\n\
|
||||
如果仍有问题,你可能需要考虑换一个新显卡或新电脑……\n\
|
||||
可在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</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 或以下。可在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</a>寻求帮助
|
||||
game.crash.reason.loading_crashed_forge=当前游戏因为模组 %1$s (%2$s) 错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.loading_crashed_fabric=当前游戏因为模组 %1$s 错误,无法继续运行。\n你可以尝试删除或更新该模组以解决问题。
|
||||
game.crash.reason.memory_exceeded=当前游戏因为分配的内存过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在游戏设置中关闭游戏内存的自动分配,并将游戏内存调低至游戏能正常启动为止。\n你还可以尝试调大系统的页面大小。
|
||||
game.crash.reason.memory_exceeded=当前游戏因为分配的内存过大,无法继续运行。\n该问题是由于系统页面文件太小导致的。\n你需要在全局(特定)游戏设置中关闭游戏内存的自动分配,并将游戏内存调低至游戏能正常启动为止。\n你还可以尝试调大系统的页面大小。
|
||||
game.crash.reason.mac_jdk_8u261=当前游戏因为你所使用的 Forge 或 OptiFine 与 Java 冲突崩溃。\n请尝试更新 Forge 和 OptiFine,或使用 Java 8u251 及更早版本启动。
|
||||
game.crash.reason.mod=当前游戏因为 %1$s 的问题,无法继续运行。\n你可以更新或删除已经安装的 %1$s 再试……
|
||||
game.crash.reason.mod_resolution=当前游戏因为 Mod 依赖问题,无法继续运行。Fabric 提供了如下信息:\n%1$s
|
||||
game.crash.reason.mod_resolution_collection=当前游戏因为前置 Mod 版本不匹配,无法继续运行。\n%1$s 需要前置 Mod:%2$s 才能继续运行。\n这表示你需要更新或降级前置。你可以到下载页的模组下载,或到网上下载 %3$s。
|
||||
@@ -408,15 +417,20 @@ game.crash.reason.mod_resolution_missing=当前游戏因为缺少 Mod 前置,
|
||||
game.crash.reason.mod_resolution_missing_minecraft=当前游戏因为 Mod 和 Minecraft 游戏版本不匹配,无法继续运行。\n%1$s 需要 Minecraft %2$s 才能运行。\n如果你要继续使用你已经安装的 Mod,你可以选择安装对应的 Minecraft 版本;如果你要继续使用当前 Minecraft 版本,你需要安装对应版本的 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.optifine_is_not_compatible_with_forge=当前游戏因为 OptiFine 与当前版本的 Forge 不兼容,导致了游戏崩溃。\n请前往 <a href="https://optifine.net/downloads">OptiFine 官网</a> 查看 OptiFine 所兼容的 Forge 版本,并严格按照对应版本重新安装游戏或在 版本设置 - 自动安装 中更换版本。\n经测试,Forge 版本过高或过低都可能导致崩溃。
|
||||
game.crash.reason.mod_files_are_decompressed=当前游戏因为 Mod 文件被解压了,无法继续运行。\n请直接把整个 Mod 文件放进 Mod 文件夹中即可!\n若解压就会导致游戏出错,请删除 Mod 文件夹中已被解压的 Mod,然后再启动游戏。
|
||||
game.crash.reason.too_many_mods_lead_to_exceeding_the_id_limit=当前游戏因为您所安装的 Mod 过多,超出了游戏的 ID 限制,无法继续运行。\n请尝试安装 <a href="https://www.curseforge.com/minecraft/mc-mods/jeid">JEID</a> 等修复 Mod,或删除部分大型 Mod。
|
||||
game.crash.reason.optifine_causes_the_world_to_fail_to_load=当前游戏可能因为 OptiFine ,无法继续运行。\n该问题只在特定 OptiFine 版本中出现,你可以尝试在 版本设置 - 自动安装 中更换 OptiFine 的版本。
|
||||
game.crash.reason.modlauncher_8=当前游戏因为您所使用的 Forge 版本与当前使用的 Java 冲突崩溃。请尝试更新 Forge,或使用 Java 8u312/11.0.13/17.0.1 及更早版本启动\n <a href="https://oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html">8u312 及更早版本下载</a> <a href="https://oracle.com/java/technologies/javase/jdk11-archive-downloads.html">11.0.13 及更早版本下载</a> <a href="https://oracle.com/java/technologies/javase/jdk17-archive-downloads.html">17.0.1 及更早版本下载</a>\n可在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</a>寻求帮助
|
||||
game.crash.reason.no_class_def_found_error=当前游戏因为代码不完整,无法继续运行。\n你的游戏可能缺失了某个 Mod,或者某些 Mod 文件不完整,或者 Mod 与游戏的版本不匹配。\n你可能需要重新安装游戏和 Mod,或请求他人帮助。\n缺失:%1$s
|
||||
game.crash.reason.no_class_def_found_error=当前游戏因为代码不完整,无法继续运行。\n你的游戏可能缺失了某个 Mod,或者某些 Mod 文件不完整,或者 Mod 与游戏的版本不匹配。\n你可能需要重新安装游戏和 Mod,或请求他人帮助。\n缺失:\n%1$s
|
||||
game.crash.reason.no_such_method_error=当前游戏因为代码不完整,无法继续运行。\n你的游戏可能缺失了某个 Mod,或者某些 Mod 文件不完整,或者 Mod 与游戏的版本不匹配。\n你可能需要重新安装游戏和 Mod,或请求他人帮助。
|
||||
game.crash.reason.opengl_not_supported=当前游戏因为你的显卡驱动存在问题,无法继续运行。\n原因是 OpenGL 不受支持,你现在是否在远程桌面或者串流模式下?如果是,请直接使用原电脑启动游戏。\n或者尝试升级你的显卡驱动到最新版本后再尝试启动游戏。如果你的电脑存在独立显卡,你需要检查游戏是否使用集成/核心显卡启动,如果是,请尝试使用独立显卡启动 HMCL 与游戏。如果仍有问题,你可能需要考虑换一个新显卡或新电脑。
|
||||
game.crash.reason.openj9=当前游戏无法运行在 OpenJ9 虚拟机上,请你在游戏设置中更换 Hotspot Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以在网上自行下载。
|
||||
game.crash.reason.out_of_memory=当前游戏因为内存不足,无法继续运行。\n这可能是内存分配太小,或者 Mod 数量过多导致的。\n你可以在游戏设置中调大游戏内存分配值以允许游戏在更大的内存下运行。\n如果仍然出现该错误,你可能需要换一台更好的电脑。
|
||||
game.crash.reason.processing_of_javaagent_failed=当前游戏因为加载 -javaagent 参数失败,无法继续运行。\n如果你在 Java 虚拟机参数 中添加了相关参数,请检查是否合法正确\n如果你没有添加相关参数或参数确认合法正确,请尝试:\n打开 控制面板 -- 时钟和区域 分类(选项为类别显示才有此选项,没有就跳过) -- 区域 -- 上方的 管理 选项卡 -- 下方的 更改系统区域设置 按钮 -- 在弹出的窗口中将 “使用 Unicode UTF-8 提供全球语言支持” 选项关闭,重启设备后再尝试启动游戏。<a href="https://gitcode.net/chearlai/f/-/raw/master/%E5%85%B3%E9%97%AD_%E4%BD%BF%E7%94%A8_Unicode_UTF-8_%E6%8F%90%E4%BE%9B%E5%85%A8%E7%90%83%E8%AF%AD%E8%A8%80%E6%94%AF%E6%8C%81_.png">操作详情</a>\n可在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</a>寻求帮助
|
||||
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 虚拟机,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://docs.microsoft.com/java/openjdk/download">此处</a> 下载微软 JDK。
|
||||
game.crash.reason.too_old_java=当前游戏因为 Java 虚拟机版本过低,无法继续运行。\n你需要在游戏设置中更换 Java %1$s 或更新版本的 Java 虚拟机,并重新启动游戏。如果没有下载安装,你可以点击 <a href="https://docs.microsoft.com/java/openjdk/download">此处</a> 下载微软 JDK。\n可在 <a href="https://discord.gg/jVvC7HfM6U">Discord</a> 或 <a href="https://kook.top/Kx7n3t">KOOK 群</a>寻求帮助
|
||||
game.crash.reason.unknown=原因未知,请点击日志按钮查看详细信息!\n<b>!!!请不要将本界面【截图】给他人!!!</b>如果你要求助他人,请你点击左下角导出游戏崩溃信息后将导出的文件发送给他人以供分析!!!
|
||||
game.crash.reason.unsatisfied_link_error=当前游戏因为缺少本地库,无法继续运行。\n这些本地库缺失:%1$s。\n如果你在游戏设置中修改了本地库路径选项,请你修改回预设模式。\n如果已经在预设模式下,请检查本地库缺失是否是 Mod 引起的,或由 HMCL 引起的。如果你确定是 HMCL 引起的,建议你向我们反馈。\n如果你确实需要自定义本地库路径,你需要保证其中包含缺失的本地库!
|
||||
game.crash.title=游戏意外退出
|
||||
@@ -463,7 +477,7 @@ install.installer.liteloader=LiteLoader
|
||||
install.installer.not_installed=不安装
|
||||
install.installer.optifine=OptiFine
|
||||
install.installer.quilt=Quilt
|
||||
install.installer.quilt-api=Quilt API
|
||||
install.installer.quilt-api=QSL/QFAPI
|
||||
install.installer.version=%s
|
||||
install.modpack=安装整合包
|
||||
install.new_game=安装新游戏版本
|
||||
@@ -518,11 +532,11 @@ launcher=启动器
|
||||
launcher.agreement=用户协议与免责声明
|
||||
launcher.agreement.accept=同意
|
||||
launcher.agreement.decline=拒绝
|
||||
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件!
|
||||
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件。
|
||||
launcher.background=背景地址
|
||||
launcher.background.choose=选择背景路径
|
||||
launcher.background.classic=经典
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg 及 bg 文件夹内的图片)
|
||||
launcher.background.default=默认(自动检索启动器同目录下的 background.png/jpg/gif 及 bg 文件夹内的图片)
|
||||
launcher.background.network=网络
|
||||
launcher.background.translucent=半透明
|
||||
launcher.cache_directory=文件下载缓存目录
|
||||
@@ -633,28 +647,43 @@ modpack.wizard.step.initialization.warning=在制作整合包前,请您确认
|
||||
modpack.wizard.step.initialization.server=点击此处查看有关服务器自动更新整合包的制作教程
|
||||
|
||||
modrinth.category.adventure=冒险
|
||||
modrinth.category.bukkit=Bukkit
|
||||
modrinth.category.bungeecord=BungeeCord
|
||||
modrinth.category.challenging=高难度
|
||||
modrinth.category.combat=战斗
|
||||
modrinth.category.cursed=Cursed
|
||||
modrinth.category.decoration=装饰
|
||||
modrinth.category.economy=经济
|
||||
modrinth.category.equipment=装备
|
||||
modrinth.category.fabric=Fabric
|
||||
modrinth.category.food=食物
|
||||
modrinth.category.forge=Forge
|
||||
modrinth.category.game-mechanics=游戏机制
|
||||
modrinth.category.kitchen-sink=大杂烩
|
||||
modrinth.category.library=支持库
|
||||
modrinth.category.lightweight=轻量
|
||||
modrinth.category.magic=魔法
|
||||
modrinth.category.management=管理
|
||||
modrinth.category.minigame=小游戏
|
||||
modrinth.category.misc=其他
|
||||
modrinth.category.mobs=生物
|
||||
modrinth.category.modloader=Modloader
|
||||
modrinth.category.multiplayer=多人
|
||||
modrinth.category.optimization=优化
|
||||
modrinth.category.paper=Paper
|
||||
modrinth.category.purpur=Purpur
|
||||
modrinth.category.quests=任务
|
||||
modrinth.category.quilt=Quilt
|
||||
modrinth.category.rift=Rift
|
||||
modrinth.category.social=社交
|
||||
modrinth.category.spigot=Spigot
|
||||
modrinth.category.sponge=Sponge
|
||||
modrinth.category.storage=存储
|
||||
modrinth.category.technology=科技
|
||||
modrinth.category.transportation=运输
|
||||
modrinth.category.utility=实用
|
||||
modrinth.category.velocity=Velocity
|
||||
modrinth.category.waterfall=Waterfall
|
||||
modrinth.category.worldgen=世界生成
|
||||
|
||||
mods=模组
|
||||
@@ -688,43 +717,66 @@ mods.restore=回退
|
||||
mods.url=官方页面
|
||||
|
||||
multiplayer=多人联机
|
||||
multiplayer.agreement.prompt=多人联机功能由 速聚 提供。使用前,你需要先同意多人联机服务提供方 速聚 的用户协议与免责声明。\n你需要了解,HMCL 仅为 速聚 提供多人联机服务入口,使用中遇到的任何问题由 速聚 负责处理。您在使用多人联机服务过程中所遇到的任何问题与纠纷(包括其付费业务)均与 HMCL 无关,应与 速聚 协商解决。
|
||||
multiplayer.download=正在下载依赖
|
||||
multiplayer.download.failed=初始化失败,部分文件未能完成下载
|
||||
multiplayer.download.success=多人联机初始化完成
|
||||
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台
|
||||
multiplayer.error.failed_get_device=HiPer 无法创建网络设备,可能是HiPer已经启动或缺少管理员权限。
|
||||
multiplayer.error.failed_load_config=HiPer 初始化失败,可能是配置文件存在问题或证书已失效。
|
||||
multiplayer.error.file_not_found=找不到 HiPer 程序。该程序应该在进入多人联机页面时完成下载。请重启 HMCL 再试。\n请检查你电脑的杀毒软件是否将 HiPer 标记为病毒,如果是,请恢复 HiPer。
|
||||
multiplayer.error.session_expired=本次使用时间已结束,若要继续使用,请更新凭证
|
||||
multiplayer.agreement.prompt=多人联机功能由 速聚 提供。使用前,你需要先同意多人联机服务提供方 速聚 的用户协议与免责声明。\n你需要了解,HMCL 仅为 速聚 提供多人联机服务入口,使用中遇到的任何问题由 速聚 负责处理。\n您在使用多人联机服务过程中所遇到的任何问题与纠纷(包括其付费业务)均与 HMCL 无关,请与 速聚 协商解决。
|
||||
multiplayer.download=正在下载依赖……
|
||||
multiplayer.download.failed=初始化失败,部分文件未能完成下载……
|
||||
multiplayer.download.success=多人联机初始化完成√
|
||||
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台!
|
||||
multiplayer.error.failed_get_device=HiPer 无法创建网络设备,可能是 HiPer 正在运行中。请关闭正在运行中的其他启动器,或通过任务管理器终止 HiPer。
|
||||
multiplayer.error.failed_load_config=HiPer 初始化失败,可能是联机索引码存在问题或索引码已过期,请检查索引码是否输入正确!你可向多人联机提供方联系寻求帮助。
|
||||
multiplayer.error.failed_sudo.linux=HiPer 需要管理员权限才能运行,你可以通过设置环境变量 HMCL_ASKPASS 配置 HiPer 如何申请权限。\n若将 HMCL_ASKPASS 设置为 'false',请通过配置 sudoers 文件允许 '%s' 免密获取 root 权限,重启后再次启动 HMCL 即可;\n若将 HMCL_ASKPASS 设置为 'user',请先配置环境变量 SUDO_ASKPASS,HMCL 将使用 sudo --askpass 启动 HiPer (具体请参见 man sudo)。
|
||||
multiplayer.error.failed_sudo.mac=HiPer 需要管理员权限才能运行。是否要授予 HiPer 管理员权限?\n点击“是”授予 HiPer 管理员权限,然后重启计算机后再次打开启动器即可联机。
|
||||
multiplayer.error.failed_sudo.windows=HiPer 需要管理员权限才能运行,请使用管理员权限重新启动 HMCL。是否查看教程?
|
||||
multiplayer.error.file_not_found=已发现 HiPer 有可用的更新,请重新进入多人联机页面以更新 HiPer。
|
||||
multiplayer.error.session_expired=本次使用时间已结束,若要继续使用,请更换联机索引码
|
||||
multiplayer.exit=HiPer 意外退出,退出码 %d
|
||||
multiplayer.hint=多人联机功能处于实验阶段,如果有问题请前往 mcer.cn 反馈
|
||||
multiplayer.powered_by=本服务由 (<a href="https://mcer.cn">速聚</a>) 授权提供 <a href\="https\://hmcl.huangyuhui.net/api/redirect/multiplayer-agreement">授权协议</a>
|
||||
multiplayer.help.1=管理员权限打开
|
||||
multiplayer.help.2=多人联机教程
|
||||
multiplayer.help.3=创建方帮助
|
||||
multiplayer.help.4=参与者帮助
|
||||
multiplayer.help.text=测试能否联机
|
||||
multiplayer.hint=多人联机服务正在维护中。
|
||||
multiplayer.hint.details=查看详情
|
||||
multiplayer.persistence=授权信息管理
|
||||
multiplayer.persistence.export=将授权信息导出至文件
|
||||
multiplayer.persistence.export.button=导出授权文件
|
||||
multiplayer.persistence.export.failed=导出授权文件失败,请检查您所选择的保存路径。
|
||||
multiplayer.persistence.export.file_not_exists=获取授权文件失败,请检查索引码是否输入正确。
|
||||
multiplayer.persistence.export.title=选择授权文件保存路径
|
||||
multiplayer.persistence.hint=此功能为长期授权 (有效期超过 24 小时) 用户提供,如果你正在使用短期授权 (有效期不超过 24 小时),则无需使用本功能。\n当你使用长期授权时,请在获取授权后的 24 小时内使用索引码启动 HiPer,否则 HiPer 将无法获取授权信息。\n随后,你可以通过下方的“导出授权信息”功能保存授权文件。\n请注意妥善保管授权文件,在获取授权 24 小时后,该文件是你的长期授权的唯一凭证,丢失该文件将无法通过索引码进行联机。
|
||||
multiplayer.persistence.import=从文件导入授权信息
|
||||
multiplayer.persistence.import.button=导入授权文件
|
||||
multiplayer.persistence.import.failed=导入授权文件失败。
|
||||
multiplayer.persistence.import.file_already_exists=授权文件已经存在。
|
||||
multiplayer.persistence.import.title=选择授权文件
|
||||
multiplayer.persistence.import.token_not_match=你选择的授权文件可能与索引码不匹配,是否继续导入?
|
||||
multiplayer.persistence.license_file=授权文件
|
||||
multiplayer.powered_by=HMCL 多人联机由 (<a href="https://mcer.cn">速聚</a>) 授权提供 <a href\="https://docs.hmcl.net/multiplayer/#%E4%BD%BF%E7%94%A8%E6%8E%88%E6%9D%83">授权协议</a>
|
||||
multiplayer.report=违法违规举报
|
||||
multiplayer.token=凭证
|
||||
multiplayer.token.apply=申请凭证
|
||||
multiplayer.token.expired=HiPer 证书过期,请重新启动 HMCL 再试。
|
||||
multiplayer.token.invalid=凭证不正确。
|
||||
multiplayer.token.malformed=HiPer 配置文件无法解析,请重新启动 HMCL 再试。
|
||||
multiplayer.token.prompt=你需要凭证才能使用多人联机服务。点击旁边的“申请凭证”查看详情
|
||||
multiplayer.off=启动 HiPer
|
||||
multiplayer.off.hint=多人联机服务由 HiPer 提供,你需要先使用管理员权限启动 HMCL(具体操作方法详见左侧的 帮助),然后输入凭证启动 HiPer 以使用多人联机服务。
|
||||
multiplayer.token=索引码
|
||||
multiplayer.token.apply=获取联机索引码
|
||||
multiplayer.token.expired=HiPer 证书过期,请更换联机索引码!
|
||||
multiplayer.token.format_invalid=索引码格式不正确
|
||||
multiplayer.token.invalid=索引码不正确。
|
||||
multiplayer.token.malformed=HiPer 配置文件无法解析,可能是输入的联机索引码有误,或是获取证书服务器宕机。\n请检查输入的联机索引码!你可向多人联机提供方联系寻求帮助。
|
||||
multiplayer.token.prompt=你需要联机索引码才能使用多人联机!点击“获取联机索引码”查看详情
|
||||
multiplayer.off.hint=由于需要注册系统网络服务权限,启动 HiPer 时将会向用户申请管理员权限。若出现认证提示,允许以启动 HiPer。
|
||||
multiplayer.off.start=启动 HiPer
|
||||
multiplayer.on=HiPer
|
||||
multiplayer.master=创建方提示
|
||||
multiplayer.master.hint=1.启动游戏\n2.选择单人游戏模式进入一个存档\n3.在游戏选项菜单内选择 对局域网开放 选项,之后你可以在游戏聊天框内看到游戏提示的端口号(通常是 5 位数字)\n4.点击下方的生成服务器地址按钮,输入端口号,你就可以获得你的服务器地址了\n5.该地址需要提供给其他需要加入服务器的玩家用于添加服务器。
|
||||
multiplayer.master.hint=1.启动游戏\n2.选择单人游戏模式进入一个存档\n3.在游戏选项菜单内选择 对局域网开放 选项,之后你可以在游戏聊天框内看到游戏提示的端口号(通常是 4~5 位数字)\n4.点击下方的生成服务器地址按钮,输入端口号,你就可以获得你的服务器地址了\n5.该地址需要提供给其他需要加入服务器的玩家用于添加服务器。
|
||||
multiplayer.master.server_address=生成服务器地址
|
||||
multiplayer.master.server_address.copy=拷贝
|
||||
multiplayer.master.video_tutorial=教学视频
|
||||
multiplayer.master.port=端口号
|
||||
multiplayer.master.video_tutorial=创建方视频教程
|
||||
multiplayer.master.port=游戏局域网开放端口号
|
||||
multiplayer.master.port.validate=在游戏聊天框中出现的端口号 (0~65535)
|
||||
multiplayer.slave=参与者提示
|
||||
multiplayer.slave.hint=1.要求创建方按照上方的 创建方提示 操作\n2.启动游戏\n3.选择多人游戏模式,选择添加服务器\n4.游戏会要求你输入服务器地址,你只需要向创建方索要服务器地址并输入,并进入服务器即可。\n- 注意:\n1.一般情况下,参与者的游戏账户必须是 微软账户 或 外置登录账户(如 Little Skin),否则你需要将服务器地址输入至下方的输入框中并点击广播,在游戏中选择多人游戏模式,进入局域网世界方可加入,具体操作方法详见左侧的 帮助 \n2.一般情况下,参与者的游戏版本、模组要必须与创建方的一致,否则加入失败。
|
||||
multiplayer.slave.hint=1.要求创建方按照上方的 创建方提示 操作\n2.启动游戏\n3.选择多人游戏模式,选择添加服务器\n4.游戏会要求你输入服务器地址,你只需要向创建方索要服务器地址并输入,并进入服务器即可。
|
||||
multiplayer.slave.hint2=1.一般情况下,参与者的游戏账户必须是 微软账户 或 外置登录账户(如 Little Skin),否则你需要将服务器地址输入至下方的输入框中并点击“加入”,在游戏中选择多人游戏模式,进入局域网世界方可加入,具体操作方法详见左侧的 帮助 \n2.一般情况下,参与者的游戏版本、模组要必须与创建方的一致,否则加入失败。
|
||||
multiplayer.slave.server_address=创建方服务器地址
|
||||
multiplayer.slave.server_address.start=加入
|
||||
multiplayer.slave.server_address.stop=退出
|
||||
multiplayer.slave.video_tutorial=视频教程
|
||||
multiplayer.session.expiration=本次使用截止时间
|
||||
multiplayer.slave.video_tutorial=参与者视频教程
|
||||
multiplayer.session.expiration=索引码到期时间
|
||||
|
||||
datapack=数据包
|
||||
datapack.add=添加数据包
|
||||
|
||||
@@ -17,11 +17,3 @@ dependencies {
|
||||
api("org.apache.commons:commons-lang3:3.12.0")
|
||||
compileOnlyApi("org.jetbrains:annotations:16.0.3")
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn(":log4j-patch:jar")
|
||||
|
||||
into("assets/game") {
|
||||
from(project(":log4j-patch").file("build/libs"))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#Wed Jul 19 18:12:48 CST 2017
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-rc-2-bin.zip
|
||||
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
path of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
path of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save ( ) {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when execute from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -1,84 +0,0 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables then windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo path of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo path of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables then windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -30,16 +30,21 @@ import java.util.UUID;
|
||||
*/
|
||||
@Immutable
|
||||
public class AuthInfo implements AutoCloseable {
|
||||
public static final String USER_TYPE_MOJANG = "mojang";
|
||||
public static final String USER_TYPE_LEGACY = "legacy";
|
||||
|
||||
|
||||
private final String username;
|
||||
private final UUID uuid;
|
||||
private final String accessToken;
|
||||
private final String userType;
|
||||
private final String userProperties;
|
||||
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, String userProperties) {
|
||||
public AuthInfo(String username, UUID uuid, String accessToken, String userType, String userProperties) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
this.accessToken = accessToken;
|
||||
this.userType = userType;
|
||||
this.userProperties = userProperties;
|
||||
}
|
||||
|
||||
@@ -55,6 +60,10 @@ public class AuthInfo implements AutoCloseable {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public String getUserType() {
|
||||
return userType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties of this user.
|
||||
* Don't know the difference between user properties and user property map.
|
||||
|
||||
@@ -253,7 +253,7 @@ public class OAuth {
|
||||
DEVICE,
|
||||
}
|
||||
|
||||
public class Result {
|
||||
public static final class Result {
|
||||
private final String accessToken;
|
||||
private final String refreshToken;
|
||||
|
||||
@@ -282,7 +282,7 @@ public class OAuth {
|
||||
@SerializedName("verification_uri")
|
||||
public String verificationURI;
|
||||
|
||||
// Life time in seconds for device_code and user_code
|
||||
// Lifetime in seconds for device_code and user_code
|
||||
@SerializedName("expires_in")
|
||||
public int expiresIn;
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
|
||||
private final String prefetchedMeta;
|
||||
|
||||
public AuthlibInjectorAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact, AuthlibInjectorServer server, String prefetchedMeta) {
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserType(), authInfo.getUserProperties());
|
||||
|
||||
this.artifact = artifact;
|
||||
this.server = server;
|
||||
|
||||
@@ -110,7 +110,7 @@ public class MicrosoftSession {
|
||||
public AuthInfo toAuthInfo() {
|
||||
requireNonNull(profile);
|
||||
|
||||
return new AuthInfo(profile.getName(), profile.getId(), accessToken, "{}");
|
||||
return new AuthInfo(profile.getName(), profile.getId(), accessToken, AuthInfo.USER_TYPE_MOJANG, "{}");
|
||||
}
|
||||
|
||||
public static class User {
|
||||
|
||||
@@ -67,6 +67,10 @@ public class OfflineAccount extends Account {
|
||||
}
|
||||
}
|
||||
|
||||
public AuthlibInjectorArtifactProvider getDownloader() {
|
||||
return downloader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UUID getUUID() {
|
||||
return uuid;
|
||||
@@ -91,7 +95,7 @@ public class OfflineAccount extends Account {
|
||||
invalidate();
|
||||
}
|
||||
|
||||
private boolean loadAuthlibInjector(Skin skin) {
|
||||
protected boolean loadAuthlibInjector(Skin skin) {
|
||||
if (skin == null) return false;
|
||||
if (skin.getType() == Skin.Type.DEFAULT) return false;
|
||||
TextureModel defaultModel = TextureModel.detectUUID(getUUID());
|
||||
@@ -104,7 +108,8 @@ public class OfflineAccount extends Account {
|
||||
|
||||
@Override
|
||||
public AuthInfo logIn() throws AuthenticationException {
|
||||
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), "{}");
|
||||
// Using "legacy" user type here because "mojang" user type may cause "invalid session token" or "disconnected" when connecting to a game server.
|
||||
AuthInfo authInfo = new AuthInfo(username, uuid, UUIDTypeAdapter.fromUUID(UUID.randomUUID()), AuthInfo.USER_TYPE_LEGACY, "{}");
|
||||
|
||||
if (loadAuthlibInjector(skin)) {
|
||||
CompletableFuture<AuthlibInjectorArtifactInfo> artifactTask = CompletableFuture.supplyAsync(() -> {
|
||||
@@ -144,7 +149,7 @@ public class OfflineAccount extends Account {
|
||||
private YggdrasilServer server;
|
||||
|
||||
public OfflineAuthInfo(AuthInfo authInfo, AuthlibInjectorArtifactInfo artifact) {
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), authInfo.getUserProperties());
|
||||
super(authInfo.getUsername(), authInfo.getUUID(), authInfo.getAccessToken(), USER_TYPE_LEGACY, authInfo.getUserProperties());
|
||||
|
||||
this.artifact = artifact;
|
||||
}
|
||||
@@ -157,7 +162,8 @@ public class OfflineAccount extends Account {
|
||||
server.start();
|
||||
|
||||
try {
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username, skin.load(username).run()));
|
||||
server.addCharacter(new YggdrasilServer.Character(uuid, username,
|
||||
skin != null ? skin.load(username).run() : null));
|
||||
} catch (IOException e) {
|
||||
// ignore
|
||||
} catch (Exception e) {
|
||||
|
||||
@@ -48,8 +48,15 @@ public class Skin {
|
||||
|
||||
public enum Type {
|
||||
DEFAULT,
|
||||
STEVE,
|
||||
ALEX,
|
||||
ARI,
|
||||
EFE,
|
||||
KAI,
|
||||
MAKENA,
|
||||
NOOR,
|
||||
STEVE,
|
||||
SUNNY,
|
||||
ZURI,
|
||||
LOCAL_FILE,
|
||||
LITTLE_SKIN,
|
||||
CUSTOM_SKIN_LOADER_API,
|
||||
@@ -59,10 +66,24 @@ public class Skin {
|
||||
switch (type) {
|
||||
case "default":
|
||||
return DEFAULT;
|
||||
case "steve":
|
||||
return STEVE;
|
||||
case "alex":
|
||||
return ALEX;
|
||||
case "ari":
|
||||
return ARI;
|
||||
case "efe":
|
||||
return EFE;
|
||||
case "kai":
|
||||
return KAI;
|
||||
case "makena":
|
||||
return MAKENA;
|
||||
case "noor":
|
||||
return NOOR;
|
||||
case "steve":
|
||||
return STEVE;
|
||||
case "sunny":
|
||||
return SUNNY;
|
||||
case "zuri":
|
||||
return ZURI;
|
||||
case "local_file":
|
||||
return LOCAL_FILE;
|
||||
case "little_skin":
|
||||
@@ -115,10 +136,24 @@ public class Skin {
|
||||
switch (type) {
|
||||
case DEFAULT:
|
||||
return Task.supplyAsync(() -> null);
|
||||
case STEVE:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/steve.png")), null));
|
||||
case ALEX:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/alex.png")), null));
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/alex.png")), null));
|
||||
case ARI:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/ari.png")), null));
|
||||
case EFE:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/efe.png")), null));
|
||||
case KAI:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/kai.png")), null));
|
||||
case MAKENA:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/makena.png")), null));
|
||||
case NOOR:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.ALEX, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/noor.png")), null));
|
||||
case STEVE:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/steve.png")), null));
|
||||
case SUNNY:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/sunny.png")), null));
|
||||
case ZURI:
|
||||
return Task.supplyAsync(() -> new LoadedSkin(TextureModel.STEVE, Texture.loadTexture(Skin.class.getResourceAsStream("/assets/img/skin/zuri.png")), null));
|
||||
case LOCAL_FILE:
|
||||
return Task.supplyAsync(() -> {
|
||||
Texture skin = null, cape = null;
|
||||
|
||||