改进 HiPer 支持 (#1763)

* Disable start hiper button when token is empty

* Use gsudo start hiper

* Automatically request administrator privileges on Windows and Linux

* Automatically start hiper with sudo on mac

* Update I18N

* Prompt user to chown unwritable files

* Update HiPer file verification failed prompt

* Log other hiper exceptions

* temporarily disable gsudo

* Detect admin rights with 'net session'

* Hide the hint pane when running with administrator privileges

* Prompt user when starting HMCL with sudo

* Fix ClassNotFoundException when no JavaFX

* Update the prompt when the permission cannot be obtained

* Add support for saving multiple HiPer configurations

* update link
close Glavo/HMCL#3

* Detect RISC-V 32

* Save original hiper authorization information

* Add support for importing and exporting license files

* Set initial license file name

* Complement the missing message

* Handling gsudo failure to obtain permission
This commit is contained in:
Glavo
2022-10-25 21:55:12 +08:00
committed by GitHub
parent b280b238df
commit 68a8944810
13 changed files with 553 additions and 97 deletions

View File

@@ -19,11 +19,15 @@ package org.jackhuang.hmcl;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.Platform; 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 javafx.stage.Stage;
import org.jackhuang.hmcl.setting.ConfigHolder; import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.setting.SambaException; import org.jackhuang.hmcl.setting.SambaException;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.AsyncTaskExecutor; import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.AwtUtils; import org.jackhuang.hmcl.ui.AwtUtils;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.upgrade.UpdateChecker;
@@ -33,13 +37,19 @@ import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.JarUtils; import org.jackhuang.hmcl.util.io.JarUtils;
import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import java.awt.*; import java.awt.*;
import java.io.IOException; import java.io.IOException;
import java.lang.management.ManagementFactory; 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.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.logging.Level; import java.util.logging.Level;
@@ -63,7 +73,51 @@ public final class Launcher extends Application {
Main.showWarningAndContinue(i18n("fatal.samba")); Main.showWarningAndContinue(i18n("fatal.samba"));
} catch (IOException e) { } catch (IOException e) {
LOG.log(Level.SEVERE, "Failed to load config", 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) { if (Metadata.HMCL_DIRECTORY.toAbsolutePath().toString().indexOf('=') >= 0) {

View File

@@ -17,6 +17,8 @@
*/ */
package org.jackhuang.hmcl; package org.jackhuang.hmcl;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import org.jackhuang.hmcl.util.Logging; import org.jackhuang.hmcl.util.Logging;
import org.jackhuang.hmcl.util.SelfDependencyPatcher; import org.jackhuang.hmcl.util.SelfDependencyPatcher;
import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.Architecture;
@@ -68,7 +70,6 @@ public final class Main {
checkJavaFX(); checkJavaFX();
Launcher.main(args); Launcher.main(args);
} }
@@ -116,6 +117,15 @@ public final class Main {
static void showErrorAndExit(String message) { static void showErrorAndExit(String message) {
System.err.println(message); System.err.println(message);
System.err.println("A fatal error has occurred, forcibly exiting."); System.err.println("A fatal error has occurred, forcibly exiting.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.ERROR, message).showAndWait();
System.exit(1);
}
} catch (Throwable ignored) {
}
JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE); JOptionPane.showMessageDialog(null, message, "Error", JOptionPane.ERROR_MESSAGE);
System.exit(1); System.exit(1);
} }
@@ -126,6 +136,14 @@ public final class Main {
static void showWarningAndContinue(String message) { static void showWarningAndContinue(String message) {
System.err.println(message); System.err.println(message);
System.err.println("Potential issues have been detected."); System.err.println("Potential issues have been detected.");
try {
if (Platform.isFxApplicationThread()) {
new Alert(Alert.AlertType.WARNING, message).showAndWait();
return;
}
} catch (Throwable ignored) {
}
JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE); JOptionPane.showMessageDialog(null, message, "Warning", JOptionPane.WARNING_MESSAGE);
} }
@@ -156,7 +174,8 @@ public final class Main {
tls.init(null, instance.getTrustManagers(), null); tls.init(null, instance.getTrustManagers(), null);
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory()); HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
LOG.info("Added Lets Encrypt root certificates as additional trust"); 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); LOG.log(Level.SEVERE, "Failed to load lets encrypt certificate. Expect problems", e);
} }
} }

View File

@@ -46,6 +46,7 @@ public final class ConfigHolder {
private static Config configInstance; private static Config configInstance;
private static GlobalConfig globalConfigInstance; private static GlobalConfig globalConfigInstance;
private static boolean newlyCreated; private static boolean newlyCreated;
private static boolean ownerChanged = false;
public static Config config() { public static Config config() {
if (configInstance == null) { if (configInstance == null) {
@@ -61,10 +62,18 @@ public final class ConfigHolder {
return globalConfigInstance; return globalConfigInstance;
} }
public static Path configLocation() {
return configLocation;
}
public static boolean isNewlyCreated() { public static boolean isNewlyCreated() {
return newlyCreated; return newlyCreated;
} }
public static boolean isOwnerChanged() {
return ownerChanged;
}
public synchronized static void init() throws IOException { public synchronized static void init() throws IOException {
if (configInstance != null) { if (configInstance != null) {
throw new IllegalStateException("Configuration is already loaded"); throw new IllegalStateException("Configuration is already loaded");
@@ -110,7 +119,7 @@ public final class ConfigHolder {
} }
private static Path locateConfig() { private static Path locateConfig() {
Path exePath = Paths.get(""); Path exePath = Paths.get("").toAbsolutePath();
try { try {
Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation() Path jarPath = Paths.get(ConfigHolder.class.getProtectionDomain().getCodeSource().getLocation()
.toURI()).toAbsolutePath(); .toURI()).toAbsolutePath();
@@ -144,6 +153,15 @@ public final class ConfigHolder {
private static Config loadConfig() throws IOException { private static Config loadConfig() throws IOException {
if (Files.exists(configLocation)) { 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 { try {
String content = FileUtils.readText(configLocation); String content = FileUtils.readText(configLocation);
Config deserialized = Config.fromJson(content); Config deserialized = Config.fromJson(content);

View File

@@ -18,16 +18,18 @@
package org.jackhuang.hmcl.ui.multiplayer; package org.jackhuang.hmcl.ui.multiplayer;
import com.google.gson.JsonParseException; import com.google.gson.JsonParseException;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.setting.ConfigHolder;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.gson.DateTypeAdapter; import org.jackhuang.hmcl.util.gson.DateTypeAdapter;
import org.jackhuang.hmcl.util.gson.JsonUtils; 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.FileUtils;
import org.jackhuang.hmcl.util.io.HttpRequest; import org.jackhuang.hmcl.util.io.HttpRequest;
import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.io.NetworkUtils;
@@ -35,33 +37,35 @@ import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder; import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.ManagedProcess; import org.jackhuang.hmcl.util.platform.ManagedProcess;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStreamWriter; import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.*;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermission;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.logging.Level; import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.util.Lang.*; import static org.jackhuang.hmcl.util.Lang.*;
import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.io.ChecksumMismatchException.verifyChecksum;
/** /**
* Cato Management. * Cato Management.
*/ */
public final class MultiplayerManager { public final class MultiplayerManager {
static final String HIPER_VERSION = "1.2.2"; // 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_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_PACKAGES_URL = HIPER_DOWNLOAD_URL + "packages.sha1";
private static final String HIPER_POINTS_URL = "https://cert.mcer.cn/point.yml"; 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"); private static final Path HIPER_TEMP_CONFIG_PATH = Metadata.HMCL_DIRECTORY.resolve("hiper.yml");
private static final Path HIPER_CONFIG_DIR = Metadata.HMCL_DIRECTORY.resolve("hiper-config");
public static final Path HIPER_PATH = getHiperLocalDirectory().resolve(getHiperFileName()); public static final Path HIPER_PATH = getHiperLocalDirectory().resolve(getHiperFileName());
public static final int HIPER_AGREEMENT_VERSION = 3; public static final int HIPER_AGREEMENT_VERSION = 3;
private static final String REMOTE_ADDRESS = "127.0.0.1"; private static final String REMOTE_ADDRESS = "127.0.0.1";
@@ -77,7 +81,7 @@ public final class MultiplayerManager {
pair(Architecture.MIPS64, "mips64"), pair(Architecture.MIPS64, "mips64"),
pair(Architecture.MIPS64EL, "mips64le"), pair(Architecture.MIPS64EL, "mips64le"),
pair(Architecture.PPC64LE, "ppc64le"), pair(Architecture.PPC64LE, "ppc64le"),
pair(Architecture.RISCV, "riscv64"), pair(Architecture.RISCV64, "riscv64"),
pair(Architecture.MIPSEL, "mipsle") pair(Architecture.MIPSEL, "mipsle")
); );
@@ -89,16 +93,57 @@ public final class MultiplayerManager {
private static final String HIPER_TARGET_NAME = String.format("%s-%s", private static final String HIPER_TARGET_NAME = String.format("%s-%s",
osMap.getOrDefault(OperatingSystem.CURRENT_OS, "windows"), osMap.getOrDefault(OperatingSystem.CURRENT_OS, "windows"),
archMap.getOrDefault(Architecture.CURRENT_ARCH, "amd64")); archMap.getOrDefault(Architecture.SYSTEM_ARCH, "amd64"));
private static final String GSUDO_VERSION = "1.7.1";
private static final String GSUDO_TARGET_ARCH = Architecture.SYSTEM_ARCH == Architecture.X86_64 ? "amd64" : "x86";
private static final String GSUDO_FILE_NAME = "gsudo.exe";
private static final String GSUDO_DOWNLOAD_URL = "https://gitcode.net/glavo/gsudo-release/-/raw/75c952ea3afe8792b0db4fe9bab87d41b21e5895/" + GSUDO_TARGET_ARCH + "/" + GSUDO_FILE_NAME;
private static final Path GSUDO_LOCAL_FILE = Metadata.HMCL_DIRECTORY.resolve("libraries").resolve("gsudo").resolve("gsudo").resolve(GSUDO_VERSION).resolve(GSUDO_TARGET_ARCH).resolve(GSUDO_FILE_NAME);
private static final boolean USE_GSUDO;
static final boolean IS_ADMINISTRATOR;
static final BooleanBinding tokenInvalid = Bindings.createBooleanBinding(
() -> !StringUtils.isAlphabeticOrNumber(globalConfig().multiplayerTokenProperty().getValue()),
globalConfig().multiplayerTokenProperty());
static {
boolean isAdministrator = false;
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
try {
Process process = Runtime.getRuntime().exec(new String[]{"net.exe", "session"});
if (!process.waitFor(1, TimeUnit.SECONDS)) {
process.destroy();
} else {
isAdministrator = process.exitValue() == 0;
}
} catch (Throwable ignored) {
}
USE_GSUDO = !isAdministrator && OperatingSystem.SYSTEM_BUILD_NUMBER >= 10000;
} else {
isAdministrator = "root".equals(System.getProperty("user.name"));
USE_GSUDO = false;
}
IS_ADMINISTRATOR = isAdministrator;
}
private static CompletableFuture<Map<String, String>> HASH; private static CompletableFuture<Map<String, String>> HASH;
private MultiplayerManager() { private MultiplayerManager() {
} }
public static Path getConfigPath(String token) {
return HIPER_CONFIG_DIR.resolve(Hex.encodeHex(DigestUtils.digest("SHA-1", token)) + ".yml");
}
public static void clearConfiguration() { public static void clearConfiguration() {
HIPER_CONFIG_PATH.toFile().delete(); try {
Files.deleteIfExists(HIPER_TEMP_CONFIG_PATH);
Files.deleteIfExists(getConfigPath(ConfigHolder.globalConfig().getMultiplayerToken()));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to delete config", e);
}
} }
private static CompletableFuture<Map<String, String>> getPackagesHash() { private static CompletableFuture<Map<String, String>> getPackagesHash() {
@@ -115,6 +160,9 @@ public final class MultiplayerManager {
LOG.warning("Failed to parse Hiper packages.sha1 file, line: " + line); LOG.warning("Failed to parse Hiper packages.sha1 file, line: " + line);
} }
} }
if (USE_GSUDO) {
hashes.put(GSUDO_FILE_NAME, HttpRequest.GET(GSUDO_DOWNLOAD_URL + ".sha1").getString().trim());
}
return hashes; return hashes;
})); }));
} }
@@ -137,11 +185,17 @@ public final class MultiplayerManager {
if (!packagesHash.containsKey(String.format("%s/hiper.exe", HIPER_TARGET_NAME))) { if (!packagesHash.containsKey(String.format("%s/hiper.exe", HIPER_TARGET_NAME))) {
throw new HiperUnsupportedPlatformException(); throw new HiperUnsupportedPlatformException();
} }
tasks = Arrays.asList( tasks = new ArrayList<>(4);
getFileDownloadTask.apply(String.format("%s/hiper.exe", HIPER_TARGET_NAME), "hiper.exe"),
getFileDownloadTask.apply(String.format("%s/wintun.dll", HIPER_TARGET_NAME), "wintun.dll") tasks.add(getFileDownloadTask.apply(String.format("%s/hiper.exe", HIPER_TARGET_NAME), "hiper.exe"));
// getFileDownloadTask.apply("tap-windows-9.21.2.exe", "tap-windows-9.21.2.exe") tasks.add(getFileDownloadTask.apply(String.format("%s/wintun.dll", HIPER_TARGET_NAME), "wintun.dll"));
); // tasks.add(getFileDownloadTask.apply("tap-windows-9.21.2.exe", "tap-windows-9.21.2.exe"));
if (USE_GSUDO)
tasks.add(new FileDownloadTask(
NetworkUtils.toURL(GSUDO_DOWNLOAD_URL),
GSUDO_LOCAL_FILE.toFile(),
new FileDownloadTask.IntegrityCheck("SHA-1", packagesHash.get(GSUDO_FILE_NAME))
));
} else { } else {
if (!packagesHash.containsKey(String.format("%s/hiper", HIPER_TARGET_NAME))) { if (!packagesHash.containsKey(String.format("%s/hiper", HIPER_TARGET_NAME))) {
throw new HiperUnsupportedPlatformException(); throw new HiperUnsupportedPlatformException();
@@ -158,14 +212,10 @@ public final class MultiplayerManager {
}); });
} }
private static void verifyChecksumAndDeleteIfNotMatched(Path file, @Nullable String expectedChecksum) throws IOException { public static void downloadHiperConfig(String token, Path configPath) throws IOException {
try { String certFileContent = HttpRequest.GET(String.format("https://cert.mcer.cn/%s.yml", token)).getString();
if (expectedChecksum != null) { if (!certFileContent.equals("")) {
ChecksumMismatchException.verifyChecksum(file, "SHA-1", expectedChecksum); FileUtils.writeText(configPath, certFileContent);
}
} catch (IOException e) {
Files.deleteIfExists(file);
throw e;
} }
} }
@@ -175,27 +225,70 @@ public final class MultiplayerManager {
throw new HiperNotExistsException(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 { try {
certFileContent = HttpRequest.GET(String.format("https://cert.mcer.cn/%s.yml", token)).getString(); if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
if (!certFileContent.equals("")) { verifyChecksum(getHiperLocalDirectory().resolve("hiper.exe"), "SHA-1", packagesHash.get(String.format("%s/hiper.exe", HIPER_TARGET_NAME)));
certFileContent += "\nlogging:\n format: json\n file_path: ./hiper.log"; verifyChecksum(getHiperLocalDirectory().resolve("wintun.dll"), "SHA-1", packagesHash.get(String.format("%s/wintun.dll", HIPER_TARGET_NAME)));
FileUtils.writeText(HIPER_CONFIG_PATH, certFileContent); // verifyChecksumAndDeleteIfNotMatched(getHiperLocalDirectory().resolve("tap-windows-9.21.2.exe"), packagesHash.get("tap-windows-9.21.2.exe"));
if (USE_GSUDO)
verifyChecksum(GSUDO_LOCAL_FILE, "SHA-1", packagesHash.get(GSUDO_FILE_NAME));
} else {
verifyChecksum(getHiperLocalDirectory().resolve("hiper"), "SHA-1", packagesHash.get(String.format("%s/hiper", HIPER_TARGET_NAME)));
} }
} catch (IOException e) { } 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); // force redownload
Files.deleteIfExists(HIPER_PATH);
throw e;
}
Path configPath = getConfigPath(token);
Files.createDirectories(configPath.getParent());
// 下载 HiPer 配置文件
Logging.registerForbiddenToken(token, "<hiper token>");
try {
downloadHiperConfig(token, configPath);
} catch (IOException e) {
LOG.log(Level.WARNING, "configuration file cloud cache token has been not available, try to use the local configuration file", e);
}
if (Files.exists(configPath)) {
Files.copy(configPath, HIPER_TEMP_CONFIG_PATH, StandardCopyOption.REPLACE_EXISTING);
try (BufferedWriter output = Files.newBufferedWriter(HIPER_TEMP_CONFIG_PATH, StandardOpenOption.WRITE, StandardOpenOption.APPEND)) {
output.write("\n");
output.write("logging:\n");
output.write(" format: json\n");
output.write(" file_path: '" + Metadata.HMCL_DIRECTORY.resolve("logs").resolve("hiper.log").toString().replace("'", "''") + "'\n");
}
}
String[] commands = new String[]{HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
if (!IS_ADMINISTRATOR) {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (USE_GSUDO)
commands = new String[]{GSUDO_LOCAL_FILE.toString(), HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
break;
case LINUX:
String askpass = System.getProperty("hmcl.askpass", System.getenv("HMCL_ASKPASS"));
if ("user".equalsIgnoreCase(askpass))
commands = new String[]{"sudo", "-A", HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
else if ("false".equalsIgnoreCase(askpass))
commands = new String[]{"sudo", "--non-interactive", HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
else {
if (Files.exists(Paths.get("/usr/bin/pkexec")))
commands = new String[]{"/usr/bin/pkexec", HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
else
commands = new String[]{"sudo", "--non-interactive", HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
}
break;
case OSX:
commands = new String[]{"sudo", "--non-interactive", HIPER_PATH.toString(), "-config", HIPER_TEMP_CONFIG_PATH.toString()};
break;
}
} }
String[] commands = new String[]{HIPER_PATH.toString(), "-config", HIPER_CONFIG_PATH.toString()};
Process process = new ProcessBuilder() Process process = new ProcessBuilder()
.command(commands) .command(commands)
.start(); .start();
@@ -213,7 +306,7 @@ public final class MultiplayerManager {
} }
public static Path getHiperLocalDirectory() { public static Path getHiperLocalDirectory() {
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve("hiper").resolve("hiper").resolve(HIPER_VERSION); return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve("hiper").resolve("hiper").resolve("binary");
} }
public static class HiperSession extends ManagedProcess { public static class HiperSession extends ManagedProcess {
@@ -238,10 +331,18 @@ public final class MultiplayerManager {
} }
private void onLog(String log) { private void onLog(String log) {
LOG.info("[Hiper] " + log); if (!log.startsWith("{")) {
LOG.warning("[HiPer] " + log);
if (log.startsWith("failed to load config"))
error = HiperExitEvent.INVALID_CONFIGURATION;
else if (log.startsWith("sudo: ") || log.startsWith("Error getting authority") || log.startsWith("Error: An error occurred trying to start process"))
error = HiperExitEvent.NO_SUDO_PRIVILEGES;
else if (log.startsWith("Failed to write to log, can't rename log file")) {
error = HiperExitEvent.NO_SUDO_PRIVILEGES;
stop();
}
if (log.contains("failed to load config")) {
error = HiperExitEvent.INVALID_CONFIGURATION;
return; return;
} }
@@ -303,6 +404,21 @@ public final class MultiplayerManager {
destroyRelatedThreads(); destroyRelatedThreads();
} }
@Override
public void stop() {
try {
writer.write("quit\n");
writer.flush();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to quit HiPer", e);
}
try {
getProcess().waitFor(1, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {
}
super.stop();
}
public EventManager<HiperExitEvent> onExit() { public EventManager<HiperExitEvent> onExit() {
return onExit; return onExit;
} }
@@ -334,6 +450,7 @@ public final class MultiplayerManager {
public static final int CERTIFICATE_EXPIRED = -3; public static final int CERTIFICATE_EXPIRED = -3;
public static final int FAILED_GET_DEVICE = -4; public static final int FAILED_GET_DEVICE = -4;
public static final int FAILED_LOAD_CONFIG = -5; public static final int FAILED_LOAD_CONFIG = -5;
public static final int NO_SUDO_PRIVILEGES = -6;
} }
public static class HiperIPEvent extends Event { public static class HiperIPEvent extends Event {

View File

@@ -26,13 +26,20 @@ import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.HMCLService; import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException; import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemUtils;
import java.io.File;
import java.util.Date; import java.util.Date;
import java.util.concurrent.CancellationException; import java.util.concurrent.CancellationException;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -186,22 +193,24 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
LOG.info("Connection rejected by the server"); LOG.info("Connection rejected by the server");
return i18n("message.cancelled"); return i18n("message.cancelled");
} else if (e instanceof MultiplayerManager.HiperInvalidConfigurationException) { } else if (e instanceof MultiplayerManager.HiperInvalidConfigurationException) {
LOG.info("Hiper invalid configuration"); LOG.warning("HiPer invalid configuration");
return i18n("multiplayer.token.malformed"); return i18n("multiplayer.token.malformed");
} else if (e instanceof MultiplayerManager.HiperNotExistsException) { } else if (e instanceof MultiplayerManager.HiperNotExistsException) {
LOG.log(Level.WARNING, "Hiper not found " + ((MultiplayerManager.HiperNotExistsException) e).getFile(), e); LOG.log(Level.WARNING, "Hiper not found " + ((MultiplayerManager.HiperNotExistsException) e).getFile(), e);
return i18n("multiplayer.error.file_not_found"); return i18n("multiplayer.error.file_not_found");
} else if (e instanceof ChecksumMismatchException) {
LOG.log(Level.WARNING, "HiPer files are not verified", e);
return i18n("multiplayer.error.file_not_found");
} else if (e instanceof MultiplayerManager.HiperExitException) { } else if (e instanceof MultiplayerManager.HiperExitException) {
LOG.info("HiPer exited accidentally");
int exitCode = ((MultiplayerManager.HiperExitException) e).getExitCode(); int exitCode = ((MultiplayerManager.HiperExitException) e).getExitCode();
LOG.warning("HiPer exited unexpectedly with exit code " + exitCode);
return i18n("multiplayer.exit", exitCode); return i18n("multiplayer.exit", exitCode);
} else if (e instanceof MultiplayerManager.HiperInvalidTokenException) { } else if (e instanceof MultiplayerManager.HiperInvalidTokenException) {
LOG.info("invalid token"); LOG.warning("invalid token");
return i18n("multiplayer.token.invalid"); return i18n("multiplayer.token.invalid");
} else if (e instanceof ChecksumMismatchException) {
return i18n("exception.artifact_malformed");
} else { } else {
return e.getLocalizedMessage(); LOG.log(Level.WARNING, "Unknown HiPer exception", e);
return e.getLocalizedMessage() + "\n" + StringUtils.getStackTrace(e);
} }
} }
@@ -281,6 +290,38 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
MultiplayerManager.clearConfiguration(); MultiplayerManager.clearConfiguration();
Controllers.dialog(i18n("multiplayer.token.malformed")); Controllers.dialog(i18n("multiplayer.token.malformed"));
break; break;
case MultiplayerManager.HiperExitEvent.NO_SUDO_PRIVILEGES:
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
Controllers.confirm(i18n("multiplayer.error.failed_sudo.windows"), null, MessageDialogPane.MessageType.WARNING, () -> {
FXUtils.openLink("https://docs.hmcl.net/multiplayer/admin.html");
}, null);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX) {
Controllers.dialog(i18n("multiplayer.error.failed_sudo.linux", MultiplayerManager.HIPER_PATH.toString()), null, MessageDialogPane.MessageType.WARNING);
} else if (OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
Controllers.confirm(i18n("multiplayer.error.failed_sudo.mac"), null, MessageDialogPane.MessageType.INFO, () -> {
try {
String text = "%hmcl-hiper ALL=(ALL:ALL) NOPASSWD: " + MultiplayerManager.HIPER_PATH.toString().replaceAll("[ @!(),:=\\\\]", "\\\\$0") + "\n";
File sudoersTmp = File.createTempFile("sudoer", ".tmp");
sudoersTmp.deleteOnExit();
FileUtils.writeText(sudoersTmp, text);
SystemUtils.callExternalProcess(
"osascript", "-e", String.format("do shell script \"%s\" with administrator privileges", String.join(";",
"dscl . create /Groups/hmcl-hiper PrimaryGroupID 758",
"dscl . merge /Groups/hmcl-hiper GroupMembership " + CommandBuilder.toShellStringLiteral(System.getProperty("user.name")) + "",
"mkdir -p /private/etc/sudoers.d",
"mv -f " + CommandBuilder.toShellStringLiteral(sudoersTmp.toString()) + " /private/etc/sudoers.d/hmcl-hiper",
"chown root /private/etc/sudoers.d/hmcl-hiper",
"chmod 0440 /private/etc/sudoers.d/hmcl-hiper"
).replaceAll("[\\\\\"]", "\\\\$0"))
);
} catch (Throwable e) {
LOG.log(Level.WARNING, "Failed to modify sudoers", e);
}
}, null);
}
break;
case MultiplayerManager.HiperExitEvent.INTERRUPTED: case MultiplayerManager.HiperExitEvent.INTERRUPTED:
// do nothing // do nothing
break; break;
@@ -303,5 +344,4 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
public ReadOnlyObjectProperty<State> stateProperty() { public ReadOnlyObjectProperty<State> stateProperty() {
return state; return state;
} }
} }

View File

@@ -20,6 +20,9 @@ package org.jackhuang.hmcl.ui.multiplayer;
import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPasswordField; import com.jfoenix.controls.JFXPasswordField;
import com.jfoenix.controls.JFXTextField; import com.jfoenix.controls.JFXTextField;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@@ -28,22 +31,35 @@ import javafx.scene.Node;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.*; import javafx.scene.layout.*;
import javafx.stage.FileChooser;
import javafx.util.StringConverter; import javafx.util.StringConverter;
import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.HMCLService; import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.Locales; import org.jackhuang.hmcl.util.i18n.Locales;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig; import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; 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.i18n.I18n.i18n;
public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimatedPageSkin<MultiplayerPage> { public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimatedPageSkin<MultiplayerPage> {
@@ -74,11 +90,11 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
item.setLeftGraphic(wrap(SVG::helpCircleOutline)); item.setLeftGraphic(wrap(SVG::helpCircleOutline));
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer")); item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer"));
}) })
.addNavigationDrawerItem(item -> { // .addNavigationDrawerItem(item -> {
item.setTitle(i18n("multiplayer.help.1")); // item.setTitle(i18n("multiplayer.help.1"));
item.setLeftGraphic(wrap(SVG::helpCircleOutline)); // item.setLeftGraphic(wrap(SVG::helpCircleOutline));
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/help.html#%E9%9B%B6%E4%BD%BF%E7%94%A8%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90%E5%90%AF%E5%8A%A8-hmcl")); // item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/admin.html"));
}) // })
.addNavigationDrawerItem(item -> { .addNavigationDrawerItem(item -> {
item.setTitle(i18n("multiplayer.help.2")); item.setTitle(i18n("multiplayer.help.2"));
item.setLeftGraphic(wrap(SVG::helpCircleOutline)); item.setLeftGraphic(wrap(SVG::helpCircleOutline));
@@ -120,7 +136,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
{ {
ComponentList offPane = new ComponentList(); ComponentList offPane = new ComponentList();
{ {
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.WARNING); HintPane hintPane = new HintPane(MessageType.WARNING);
hintPane.setText(i18n("multiplayer.off.hint")); hintPane.setText(i18n("multiplayer.off.hint"));
BorderPane tokenPane = new BorderPane(); BorderPane tokenPane = new BorderPane();
@@ -136,6 +152,12 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
tokenField.textProperty().bindBidirectional(globalConfig().multiplayerTokenProperty()); tokenField.textProperty().bindBidirectional(globalConfig().multiplayerTokenProperty());
tokenField.setPromptText(i18n("multiplayer.token.prompt")); tokenField.setPromptText(i18n("multiplayer.token.prompt"));
Validator validator = new Validator("multiplayer.token.format_invalid", StringUtils::isAlphabeticOrNumber);
InvalidationListener listener = any -> tokenField.validate();
validator.getProperties().put(validator, listener);
tokenField.textProperty().addListener(new WeakInvalidationListener(listener));
tokenField.getValidators().add(validator);
JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.token.apply")); JFXHyperlink applyLink = new JFXHyperlink(i18n("multiplayer.token.apply"));
BorderPane.setAlignment(applyLink, Pos.CENTER_RIGHT); BorderPane.setAlignment(applyLink, Pos.CENTER_RIGHT);
applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token")); applyLink.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-static-token"));
@@ -148,12 +170,15 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
startButton.getStyleClass().add("jfx-button-raised"); startButton.getStyleClass().add("jfx-button-raised");
startButton.setButtonType(JFXButton.ButtonType.RAISED); startButton.setButtonType(JFXButton.ButtonType.RAISED);
startButton.setOnMouseClicked(e -> control.start()); startButton.setOnMouseClicked(e -> control.start());
startButton.disableProperty().bind(MultiplayerManager.tokenInvalid);
startPane.getChildren().setAll(startButton); startPane.getChildren().setAll(startButton);
startPane.setAlignment(Pos.CENTER_RIGHT); startPane.setAlignment(Pos.CENTER_RIGHT);
} }
offPane.getContent().setAll(hintPane, tokenPane, startPane); if (!MultiplayerManager.IS_ADMINISTRATOR)
offPane.getContent().add(hintPane);
offPane.getContent().addAll(tokenPane, startPane);
} }
ComponentList onPane = new ComponentList(); ComponentList onPane = new ComponentList();
@@ -186,7 +211,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-master")); tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-master"));
masterPane.addRow(0, titlePane); masterPane.addRow(0, titlePane);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); HintPane hintPane = new HintPane(MessageType.INFO);
GridPane.setColumnSpan(hintPane, 3); GridPane.setColumnSpan(hintPane, 3);
hintPane.setText(i18n("multiplayer.master.hint")); hintPane.setText(i18n("multiplayer.master.hint"));
masterPane.addRow(1, hintPane); masterPane.addRow(1, hintPane);
@@ -236,12 +261,12 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-slave")); tutorial.setOnAction(e -> HMCLService.openRedirectLink("multiplayer-tutorial-slave"));
titlePane.setRight(tutorial); titlePane.setRight(tutorial);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); HintPane hintPane = new HintPane(MessageType.INFO);
GridPane.setColumnSpan(hintPane, 3); GridPane.setColumnSpan(hintPane, 3);
hintPane.setText(i18n("multiplayer.slave.hint")); hintPane.setText(i18n("multiplayer.slave.hint"));
slavePane.getChildren().add(hintPane); slavePane.getChildren().add(hintPane);
HintPane hintPane2 = new HintPane(MessageDialogPane.MessageType.WARNING); HintPane hintPane2 = new HintPane(MessageType.WARNING);
GridPane.setColumnSpan(hintPane2, 3); GridPane.setColumnSpan(hintPane2, 3);
hintPane2.setText(i18n("multiplayer.slave.hint2")); hintPane2.setText(i18n("multiplayer.slave.hint2"));
slavePane.getChildren().add(hintPane2); slavePane.getChildren().add(hintPane2);
@@ -307,6 +332,107 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
}); });
} }
ComponentList persistencePane = new ComponentList();
{
HintPane hintPane = new HintPane(MessageType.WARNING);
hintPane.setText(i18n("multiplayer.persistence.hint"));
BorderPane importPane = new BorderPane();
{
Label left = new Label(i18n("multiplayer.persistence.import"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
importPane.setLeft(left);
JFXButton importButton = new JFXButton(i18n("multiplayer.persistence.import.button"));
importButton.setOnMouseClicked(e -> {
Path targetPath = MultiplayerManager.getConfigPath(globalConfig().getMultiplayerToken());
if (Files.exists(targetPath)) {
LOG.warning("License file " + targetPath + " already exists");
Controllers.dialog(i18n("multiplayer.persistence.import.file_already_exists"), null, MessageType.ERROR);
return;
}
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("multiplayer.persistence.import.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("multiplayer.persistence.license_file"), "*.yml"));
File file = fileChooser.showOpenDialog(Controllers.getStage());
if (file == null)
return;
CompletableFuture<Boolean> future = new CompletableFuture<>();
if (file.getName().matches("[a-z0-9]{40}.yml") && !targetPath.getFileName().toString().equals(file.getName())) {
Controllers.confirm(i18n("multiplayer.persistence.import.token_not_match"), null, MessageType.QUESTION,
() -> future.complete(true),
() -> future.complete(false)) ;
} else {
future.complete(true);
}
future.thenAcceptAsync(Lang.wrapConsumer(c -> {
if (c) Files.copy(file.toPath(), targetPath);
})).exceptionally(exception -> {
LOG.log(Level.WARNING, "Failed to import license file", exception);
Platform.runLater(() -> Controllers.dialog(i18n("multiplayer.persistence.import.failed"), null, MessageType.ERROR));
return null;
});
});
importButton.disableProperty().bind(MultiplayerManager.tokenInvalid);
importButton.getStyleClass().add("jfx-button-border");
importPane.setRight(importButton);
}
BorderPane exportPane = new BorderPane();
{
Label left = new Label(i18n("multiplayer.persistence.export"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
exportPane.setLeft(left);
JFXButton exportButton = new JFXButton(i18n("multiplayer.persistence.export.button"));
exportButton.setOnMouseClicked(e -> {
String token = globalConfig().getMultiplayerToken();
Path configPath = MultiplayerManager.getConfigPath(token);
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(i18n("multiplayer.persistence.export.title"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("multiplayer.persistence.license_file"), "*.yml"));
fileChooser.setInitialFileName(configPath.getFileName().toString());
File file = fileChooser.showSaveDialog(Controllers.getStage());
if (file == null)
return;
CompletableFuture.runAsync(Lang.wrap(() -> MultiplayerManager.downloadHiperConfig(token, configPath)), Schedulers.io())
.handleAsync((ignored, exception) -> {
if (exception != null) {
LOG.log(Level.INFO, "Unable to download hiper config file", e);
}
if (!Files.isRegularFile(configPath)) {
LOG.warning("License file " + configPath + " not exists");
Platform.runLater(() -> Controllers.dialog(i18n("multiplayer.persistence.export.file_not_exists"), null, MessageType.ERROR));
return null;
}
try {
Files.copy(configPath, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ioException) {
LOG.log(Level.WARNING, "Failed to export license file", ioException);
Platform.runLater(() -> Controllers.dialog(i18n("multiplayer.persistence.export.failed"), null, MessageType.ERROR));
}
return null;
});
});
exportButton.disableProperty().bind(MultiplayerManager.tokenInvalid);
exportButton.getStyleClass().add("jfx-button-border");
exportPane.setRight(exportButton);
}
persistencePane.getContent().setAll(hintPane, importPane, exportPane);
}
ComponentList thanksPane = new ComponentList(); ComponentList thanksPane = new ComponentList();
{ {
HBox pane = new HBox(); HBox pane = new HBox();
@@ -329,6 +455,8 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
content.getChildren().setAll( content.getChildren().setAll(
mainPane, mainPane,
ComponentList.createComponentListTitle(i18n("multiplayer.persistence")),
persistencePane,
ComponentList.createComponentListTitle(i18n("about")), ComponentList.createComponentListTitle(i18n("about")),
thanksPane thanksPane
); );

View File

@@ -168,6 +168,7 @@ assets.index.malformed=Index files of downloaded assets were corrupted. You can
button.cancel=Cancel button.cancel=Cancel
button.change_source=Change Download Source button.change_source=Change Download Source
button.clear=Clear button.clear=Clear
button.copy_and_exit=Copy and Exit
button.delete=Delete button.delete=Delete
button.edit=Edit button.edit=Edit
button.install=Install button.install=Install
@@ -356,8 +357,12 @@ 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\ 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\ \n\
Please update your Java version to at least 8u101 to fix this issue. 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\ 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. 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.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\ fatal.apply_update_failure=We are sorry, but Hello Minecraft\! Launcher is unable to update.\n\
\n\ \n\
@@ -850,7 +855,10 @@ multiplayer.download.success=Multiplayer online initialization completed
multiplayer.download.unsupported=Multiplayer dependencies are not supported on current system or platform 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_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.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=An update to HiPer has been found. Please re-enter the multiplayer online page or reopen the launcher! \nIf the error is still prompted, please check whether the anti-virus software of your computer has marked HiPer as a virus. If so, please 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.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.exit=HiPer exited unexpectedly with exit code %d
multiplayer.help.1=管理员权限打开 multiplayer.help.1=管理员权限打开
@@ -859,16 +867,31 @@ multiplayer.help.3=创建方帮助
multiplayer.help.4=参与者帮助 multiplayer.help.4=参与者帮助
multiplayer.help.text=测试能否联机 multiplayer.help.text=测试能否联机
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.hint=The multiplayer online function is in the experimental stage, if you have any questions, please go to mcer.cn to give feedback
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.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.report=Illegal and Violation Report
multiplayer.session.name.motd=HMCL Multiplayer Session multiplayer.session.name.motd=HMCL Multiplayer Session
multiplayer.token=Token multiplayer.token=Token
multiplayer.token.apply=Apply for a token multiplayer.token.apply=Apply for a token
multiplayer.token.expired=HiPer certificate expired, please restart HMCL and try again. multiplayer.token.expired=HiPer token expired, please restart HMCL and try again.
multiplayer.token.invalid=Invalid credentials. 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.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.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.hint=Please start HMCL with administrator privileges, otherwise it will not work! 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.off.start=Start HiPer
multiplayer.master=Prompt for creator 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.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.

View File

@@ -155,6 +155,7 @@ assets.index.malformed=資源檔案的索引檔案損壞,您可以在遊戲 [
button.cancel=取消 button.cancel=取消
button.change_source=切換下載源 button.change_source=切換下載源
button.clear=清除 button.clear=清除
button.copy_and_exit=複製並退出
button.delete=刪除 button.delete=刪除
button.edit=編輯 button.edit=編輯
button.install=安裝 button.install=安裝
@@ -330,7 +331,9 @@ 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.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.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.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=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.migration_requires_manual_reboot=Hello Minecraft! Launcher 即將升級完成,請重新開啟 Hello Minecraft! Launcher。
fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 Hello Minecraft! Launcher 來完成升級。\n請考慮向我們回報該問題。 fatal.apply_update_failure=我們很抱歉 Hello Minecraft! Launcher 無法自動完成升級程式,因為出現了一些問題。\n但你依然可以從 %s 處手動下載 Hello Minecraft! Launcher 來完成升級。\n請考慮向我們回報該問題。
fatal.samba=如果您正在通過 Samba 共亯的資料夾中運行 HMCLHMCL 可能無法正常工作,請嘗試更新您的 Java 或在本地資料夾內運行 HMCL。 fatal.samba=如果您正在通過 Samba 共亯的資料夾中運行 HMCLHMCL 可能無法正常工作,請嘗試更新您的 Java 或在本地資料夾內運行 HMCL。
@@ -696,10 +699,13 @@ multiplayer.download=正在下載相依元件
multiplayer.download.failed=初始化失敗,部分檔案未能完成下載 multiplayer.download.failed=初始化失敗,部分檔案未能完成下載
multiplayer.download.success=多人聯機初始化完成 multiplayer.download.success=多人聯機初始化完成
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台 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.failed_load_config=HiPer 初始化失敗,可能是配置文件存在問題或證書已失效。
multiplayer.error.file_not_found=已發現 HiPer 有可用的更新,請重新進入多人聯機頁面或重新打開啟動器!\n若任然見到該提示可能是電腦的殺毒軟體將 HiPer 標記為病毒如果是請恢復HiPer multiplayer.error.failed_sudo.linux=HiPer 需要管理員許可權才能運行,您可以通過設置環境變量 HMCL_ASKPASS 配置 HiPer 如何申請權限。\n若將 HMCL_ASKPASS 設置為 'false',請通過配置 sudoers 文件允許 '%s' 免密獲取 root 權限,重啟後再次啟動 HMCL 即可;\n若將 HMCL_ASKPASS 設置為 'user',請先設置環境變量 SUDO_ASKPASSHMCL 將使用 sudo --askpass 啟動 HiPer (具體請參見 man sudo)
multiplayer.error.session_expired=本次使用時間已結束,若要繼續使用,請更新憑證 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.exit=HiPer 意外退出,退出碼 %d
multiplayer.help.1=管理員權限打開 multiplayer.help.1=管理員權限打開
multiplayer.help.2=多人聯機教程 multiplayer.help.2=多人聯機教程
@@ -707,15 +713,30 @@ multiplayer.help.3=創建方幫助
multiplayer.help.4=參與者幫助 multiplayer.help.4=參與者幫助
multiplayer.help.text=測試能否聯機 multiplayer.help.text=測試能否聯機
multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請前往 mcer.cn 回饋 multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請前往 mcer.cn 回饋
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.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.report=違法違規檢舉
multiplayer.token=憑證(索引碼) multiplayer.token=索引碼
multiplayer.token.apply=申請憑證 multiplayer.token.apply=申請索引碼
multiplayer.token.invalid=憑證不正確 multiplayer.token.format_invalid=索引碼格式不正確
multiplayer.token.invalid=索引碼不正確。
multiplayer.token.expired=HiPer 證書過期,請重新啟動 HMCL 再試。 multiplayer.token.expired=HiPer 證書過期,請重新啟動 HMCL 再試。
multiplayer.token.malformed=HiPer 配置檔案無法解析,請重新啟動 HMCL 再試。 multiplayer.token.malformed=HiPer 配置檔案無法解析,請重新啟動 HMCL 再試。
multiplayer.token.prompt=你需要憑證才能使用多人聯機服務。點擊旁邊的“申請憑證”查看詳情 multiplayer.token.prompt=你需要索引碼才能使用多人聯機服務。點擊旁邊的“申請索引碼”查看詳情
multiplayer.off.hint=請使用管理員權限啟動 HMCL否則無法使用詳細請查看左側的幫助 multiplayer.off.hint=由於需要注册系統網路服務許可權,啟動 HiPer 時將會向用戶申請管理員許可權。若出現認證提示,允許以啟動 HiPer。
multiplayer.off.start=啟動 HiPer multiplayer.off.start=啟動 HiPer
multiplayer.master=创建方提示 multiplayer.master=创建方提示
multiplayer.master.hint=1.啟動遊戲\n2.選擇單人遊戲模式進入一個存檔\n3.在遊戲選項菜單內選擇 對局域網開放 選項,之後你可以在遊戲聊天框內看到遊戲提示的端口號(通常是 4~5 位數字)\n4.點擊下方的生成伺服器地址按鈕,輸入端口號,你就可以獲得你的伺服器地址了\n5.該地址需要提供給其他需要加入伺服器的玩家用於添加伺服器。 multiplayer.master.hint=1.啟動遊戲\n2.選擇單人遊戲模式進入一個存檔\n3.在遊戲選項菜單內選擇 對局域網開放 選項,之後你可以在遊戲聊天框內看到遊戲提示的端口號(通常是 4~5 位數字)\n4.點擊下方的生成伺服器地址按鈕,輸入端口號,你就可以獲得你的伺服器地址了\n5.該地址需要提供給其他需要加入伺服器的玩家用於添加伺服器。

View File

@@ -155,6 +155,7 @@ assets.index.malformed=资源文件的索引文件损坏,您可以在相应的
button.cancel=取消 button.cancel=取消
button.change_source=切换下载源 button.change_source=切换下载源
button.clear=清除 button.clear=清除
button.copy_and_exit=复制并退出
button.delete=删除 button.delete=删除
button.edit=修改 button.edit=修改
button.install=安装 button.install=安装
@@ -330,7 +331,9 @@ 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.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 8java.com或者安装 BellSoft Liberica Full JREbell-sw.com/pages/downloads/?package=jre-full。\n如果您使用的是其他 OpenJDK 构建,请确保其包含 OpenJFX fatal.javafx.missing=缺少 JavaFX 运行环境。\n如果您使用的是 Java 11 或更高版本,请降级到 Oracle JRE 8java.com或者安装 BellSoft Liberica Full JREbell-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.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.migration_requires_manual_reboot=Hello Minecraft! Launcher 即将完成升级,请重新打开 Hello Minecraft! Launcher。
fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题……\n但你依可以从 %s 处手动下载 Hello Minecraft! Launcher 来完成升级\n请考虑向我们反馈该问题 fatal.apply_update_failure=我们很抱歉 Hello Minecraft! Launcher 无法自动完成升级,因为出现了一些问题……\n但你依可以从 %s 处手动下载 Hello Minecraft! Launcher 来完成升级\n请考虑向我们反馈该问题
fatal.samba=如果您正在通过 Samba 共享的文件夹中运行 HMCLHMCL 可能无法正常工作,请尝试更新您的 Java 或在本地文件夹内运行 HMCL。 fatal.samba=如果您正在通过 Samba 共享的文件夹中运行 HMCLHMCL 可能无法正常工作,请尝试更新您的 Java 或在本地文件夹内运行 HMCL。
@@ -521,7 +524,7 @@ launcher=启动器
launcher.agreement=用户协议与免责声明 launcher.agreement=用户协议与免责声明
launcher.agreement.accept=同意 launcher.agreement.accept=同意
launcher.agreement.decline=拒绝 launcher.agreement.decline=拒绝
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件 launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件
launcher.background=背景地址 launcher.background=背景地址
launcher.background.choose=选择背景路径 launcher.background.choose=选择背景路径
launcher.background.classic=经典 launcher.background.classic=经典
@@ -691,15 +694,18 @@ mods.restore=回退
mods.url=官方页面 mods.url=官方页面
multiplayer=多人联机 multiplayer=多人联机
multiplayer.agreement.prompt=多人联机功能由 速聚 提供。使用前,你需要先同意多人联机服务提供方 速聚 的用户协议与免责声明。\n你需要了解HMCL 仅为 速聚 提供多人联机服务入口,使用中遇到的任何问题由 速聚 负责处理。\n您在使用多人联机服务过程中所遇到的任何问题与纠纷包括其付费业务均与 HMCL 无关!应与 速聚 协商解决 multiplayer.agreement.prompt=多人联机功能由 速聚 提供。使用前,你需要先同意多人联机服务提供方 速聚 的用户协议与免责声明。\n你需要了解HMCL 仅为 速聚 提供多人联机服务入口,使用中遇到的任何问题由 速聚 负责处理。\n您在使用多人联机服务过程中所遇到的任何问题与纠纷包括其付费业务均与 HMCL 无关,请与 速聚 协商解决
multiplayer.download=正在下载依赖…… multiplayer.download=正在下载依赖……
multiplayer.download.failed=初始化失败,部分文件未能完成下载…… multiplayer.download.failed=初始化失败,部分文件未能完成下载……
multiplayer.download.success=多人联机初始化完成√ multiplayer.download.success=多人联机初始化完成√
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台! multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台!
multiplayer.error.failed_get_device=HiPer 无法创建网络设备,可能是 HiPer 已经在后台启动(不能开两个 HiPer )或未使用管理员身份启动 HMCL详细请查看左侧的帮助! multiplayer.error.failed_get_device=HiPer 无法创建网络设备,可能是 HiPer 正在运行中。请关闭正在运行中的其他启动器,或通过任务管理器终止 HiPer。
multiplayer.error.failed_load_config=HiPer 初始化失败,可能是联机凭证存在问题或凭证已过期,请检查凭证是否输入正确!你可向多人联机提供方联系寻求帮助。 multiplayer.error.failed_load_config=HiPer 初始化失败,可能是联机索引码存在问题或索引码已过期,请检查索引码是否输入正确!你可向多人联机提供方联系寻求帮助。
multiplayer.error.file_not_found=已发现 HiPer 有可用的更新,请重新进入多人联机页面或重新打开启动器以更新 HiPer \n若任然见到该提示可能是电脑的杀毒软件将 HiPer 标记为病毒,如果是,请恢复 HiPer multiplayer.error.failed_sudo.linux=HiPer 需要管理员权限才能运行,你可以通过设置环境变量 HMCL_ASKPASS 配置 HiPer 如何申请权限。\n若将 HMCL_ASKPASS 设置为 'false',请通过配置 sudoers 文件允许 '%s' 免密获取 root 权限,重启后再次启动 HMCL 即可;\n若将 HMCL_ASKPASS 设置为 'user',请先配置环境变量 SUDO_ASKPASSHMCL 将使用 sudo --askpass 启动 HiPer (具体请参见 man sudo)。
multiplayer.error.session_expired=本次使用时间已结束,若要继续使用,请更换联机凭证! 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.exit=HiPer 意外退出,退出码 %d
multiplayer.help.1=管理员权限打开 multiplayer.help.1=管理员权限打开
multiplayer.help.2=多人联机教程 multiplayer.help.2=多人联机教程
@@ -707,15 +713,30 @@ multiplayer.help.3=创建方帮助
multiplayer.help.4=参与者帮助 multiplayer.help.4=参与者帮助
multiplayer.help.text=测试能否联机 multiplayer.help.text=测试能否联机
multiplayer.hint=多人联机功能处于实验阶段,如果有问题请前往 mcer.cn 反馈 multiplayer.hint=多人联机功能处于实验阶段,如果有问题请前往 mcer.cn 反馈
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.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.report=违法违规举报
multiplayer.token=凭证(索引码) multiplayer.token=索引码
multiplayer.token.apply=获取联机凭证 multiplayer.token.apply=获取联机索引码
multiplayer.token.expired=HiPer 证书过期,请更换联机凭证 multiplayer.token.expired=HiPer 证书过期,请更换联机索引码
multiplayer.token.invalid=凭证不正确 multiplayer.token.format_invalid=索引码格式不正确
multiplayer.token.malformed=HiPer 配置文件无法解析,可能是输入的联机凭证有误,或是获取证书服务器宕机。\n请检查输入的联机凭证你可向多人联机提供方联系寻求帮助 multiplayer.token.invalid=索引码不正确
multiplayer.token.prompt=你需要联机凭证才能使用多人联机!点击“获取联机凭证”查看详情 multiplayer.token.malformed=HiPer 配置文件无法解析,可能是输入的联机索引码有误,或是获取证书服务器宕机。\n请检查输入的联机索引码你可向多人联机提供方联系寻求帮助。
multiplayer.off.hint=请使用管理员权限启动 HMCL否则将无法使用详细请查看左侧的帮助 multiplayer.token.prompt=你需要联机索引码才能使用多人联机!点击“获取联机索引码”查看详情
multiplayer.off.hint=由于需要注册系统网络服务权限,启动 HiPer 时将会向用户申请管理员权限。若出现认证提示,允许以启动 HiPer。
multiplayer.off.start=启动 HiPer multiplayer.off.start=启动 HiPer
multiplayer.master=创建方提示 multiplayer.master=创建方提示
multiplayer.master.hint=1.启动游戏\n2.选择单人游戏模式进入一个存档\n3.在游戏选项菜单内选择 对局域网开放 选项,之后你可以在游戏聊天框内看到游戏提示的端口号(通常是 4~5 位数字)\n4.点击下方的生成服务器地址按钮,输入端口号,你就可以获得你的服务器地址了\n5.该地址需要提供给其他需要加入服务器的玩家用于添加服务器。 multiplayer.master.hint=1.启动游戏\n2.选择单人游戏模式进入一个存档\n3.在游戏选项菜单内选择 对局域网开放 选项,之后你可以在游戏聊天框内看到游戏提示的端口号(通常是 4~5 位数字)\n4.点击下方的生成服务器地址按钮,输入端口号,你就可以获得你的服务器地址了\n5.该地址需要提供给其他需要加入服务器的玩家用于添加服务器。

View File

@@ -67,6 +67,9 @@ public final class Logging {
}); });
try { try {
if (Files.isRegularFile(logFolder))
Files.delete(logFolder);
Files.createDirectories(logFolder); Files.createDirectories(logFolder);
FileHandler fileHandler = new FileHandler(logFolder.resolve("hmcl.log").toAbsolutePath().toString()); FileHandler fileHandler = new FileHandler(logFolder.resolve("hmcl.log").toAbsolutePath().toString());
fileHandler.setLevel(Level.FINEST); fileHandler.setLevel(Level.FINEST);
@@ -74,7 +77,7 @@ public final class Logging {
fileHandler.setEncoding("UTF-8"); fileHandler.setEncoding("UTF-8");
LOG.addHandler(fileHandler); LOG.addHandler(fileHandler);
} catch (IOException e) { } catch (IOException e) {
System.err.println("Unable to create hmcl.log, " + e.getMessage()); System.err.println("Unable to create hmcl.log\n" + StringUtils.getStackTrace(e));
} }
ConsoleHandler consoleHandler = new ConsoleHandler(); ConsoleHandler consoleHandler = new ConsoleHandler();

View File

@@ -260,6 +260,16 @@ public final class StringUtils {
return US_ASCII_ENCODER.canEncode(cs); return US_ASCII_ENCODER.canEncode(cs);
} }
public static boolean isAlphabeticOrNumber(String str) {
int length = str.length();
for (int i = 0; i < length; i++) {
char ch = str.charAt(i);
if (!(ch >= '0' && ch <= '9') && !(ch >= 'a' && ch <= 'z') && !(ch >= 'A' && ch <= 'Z'))
return false;
}
return true;
}
/** /**
* Class for computing the longest common subsequence between strings. * Class for computing the longest common subsequence between strings.
*/ */

View File

@@ -46,7 +46,8 @@ public enum Architecture {
PPC64LE(BIT_64, "PowerPC-64 (Little-Endian)"), PPC64LE(BIT_64, "PowerPC-64 (Little-Endian)"),
S390(BIT_32), S390(BIT_32),
S390X(BIT_64, "S390x"), S390X(BIT_64, "S390x"),
RISCV(BIT_64, "RISC-V"), RISCV32(BIT_32, "RISC-V (32 Bit)"),
RISCV64(BIT_64, "RISC-V"),
LOONGARCH32(BIT_32, "LoongArch32"), LOONGARCH32(BIT_32, "LoongArch32"),
LOONGARCH64_OW(BIT_64, "LoongArch64 (old world)"), LOONGARCH64_OW(BIT_64, "LoongArch64 (old world)"),
LOONGARCH64(BIT_64, "LoongArch64"), LOONGARCH64(BIT_64, "LoongArch64"),
@@ -140,7 +141,8 @@ public enum Architecture {
return MIPSEL; return MIPSEL;
case "riscv": case "riscv":
case "risc-v": case "risc-v":
return RISCV; case "riscv64":
return RISCV64;
case "ia64": case "ia64":
case "ia64w": case "ia64w":
case "itanium64": case "itanium64":

View File

@@ -131,11 +131,11 @@ public class ManagedProcess {
} }
public synchronized void pumpInputStream(Consumer<String> onLogLine) { public synchronized void pumpInputStream(Consumer<String> onLogLine) {
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), onLogLine), "ProcessInputStreamPump", true)); addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessInputStreamPump", true));
} }
public synchronized void pumpErrorStream(Consumer<String> onLogLine) { public synchronized void pumpErrorStream(Consumer<String> onLogLine) {
addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), onLogLine), "ProcessErrorStreamPump", true)); addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), onLogLine, OperatingSystem.NATIVE_CHARSET), "ProcessErrorStreamPump", true));
} }
/** /**