Remove HiPer Support
This commit is contained in:
@@ -47,8 +47,6 @@ val microsoftAuthId = System.getenv("MICROSOFT_AUTH_ID") ?: ""
|
||||
val microsoftAuthSecret = System.getenv("MICROSOFT_AUTH_SECRET") ?: ""
|
||||
val curseForgeApiKey = System.getenv("CURSEFORGE_API_KEY") ?: ""
|
||||
|
||||
val enableHiPer = System.getenv("ENABLE_HIPER") ?: "false"
|
||||
|
||||
version = "$versionRoot.$buildNumber"
|
||||
|
||||
dependencies {
|
||||
@@ -151,7 +149,6 @@ tasks.getByName<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar>("sha
|
||||
"Microsoft-Auth-Secret" to microsoftAuthSecret,
|
||||
"CurseForge-Api-Key" to curseForgeApiKey,
|
||||
"Build-Channel" to versionType,
|
||||
"Enable-HiPer" to enableHiPer,
|
||||
"Class-Path" to "pack200.jar",
|
||||
"Add-Opens" to listOf(
|
||||
"java.base/java.lang",
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -44,7 +44,6 @@ import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.upgrade.UpdateChecker;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
import org.jackhuang.hmcl.util.io.CompressingUtils;
|
||||
import org.jackhuang.hmcl.util.io.JarUtils;
|
||||
import org.jackhuang.hmcl.util.versioning.VersionNumber;
|
||||
|
||||
import java.io.File;
|
||||
@@ -156,18 +155,13 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
|
||||
multiplayerItem.setLeftGraphic(wrap(SVG::lan));
|
||||
multiplayerItem.setActionButtonVisible(false);
|
||||
multiplayerItem.setTitle(i18n("multiplayer"));
|
||||
if ("true".equalsIgnoreCase(JarUtils.getManifestAttribute("Enable-HiPer", "")))
|
||||
multiplayerItem.setOnAction(e -> Controllers.navigate(Controllers.getMultiplayerPage()));
|
||||
else {
|
||||
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()));
|
||||
}
|
||||
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();
|
||||
|
||||
@@ -1,153 +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.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;
|
||||
}
|
||||
|
||||
private Thread newThread(Runnable task, String name) {
|
||||
Thread thread = new Thread(threadGroup, task, name);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
|
||||
@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 = newThread(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 = newThread(() -> 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");
|
||||
newThread(() -> forwardTraffic(forwardingSocket, forwardedSocket), "Forward S->D").start();
|
||||
newThread(() -> 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,553 +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 javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.event.EventManager;
|
||||
import org.jackhuang.hmcl.setting.ConfigHolder;
|
||||
import org.jackhuang.hmcl.task.FileDownloadTask;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.util.*;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
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 java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiFunction;
|
||||
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.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
import static org.jackhuang.hmcl.util.io.ChecksumMismatchException.verifyChecksum;
|
||||
|
||||
/**
|
||||
* 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_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 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.RISCV64, "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.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(
|
||||
() -> {
|
||||
String token = globalConfig().multiplayerTokenProperty().getValue();
|
||||
return token == null || token.isEmpty() || !StringUtils.isAlphabeticOrNumber(token);
|
||||
},
|
||||
globalConfig().multiplayerTokenProperty());
|
||||
|
||||
private static final DateFormat HIPER_VALID_TIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
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 MultiplayerManager() {
|
||||
}
|
||||
|
||||
public static Path getConfigPath(String token) {
|
||||
return HIPER_CONFIG_DIR.resolve(Hex.encodeHex(DigestUtils.digest("SHA-1", token)) + ".yml");
|
||||
}
|
||||
|
||||
public static void clearConfiguration() {
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (USE_GSUDO) {
|
||||
hashes.put(GSUDO_FILE_NAME, HttpRequest.GET(GSUDO_DOWNLOAD_URL + ".sha1").getString().trim());
|
||||
}
|
||||
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 = new ArrayList<>(4);
|
||||
|
||||
tasks.add(getFileDownloadTask.apply(String.format("%s/hiper.exe", HIPER_TARGET_NAME), "hiper.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 {
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public static void downloadHiperConfig(String token, Path configPath) throws IOException {
|
||||
String certFileContent = HttpRequest.GET(String.format("https://cert.mcer.cn/%s.yml", token)).getString();
|
||||
if (!certFileContent.equals("")) {
|
||||
FileUtils.writeText(configPath, certFileContent);
|
||||
}
|
||||
}
|
||||
|
||||
public static CompletableFuture<HiperSession> startHiper(String token) {
|
||||
return getPackagesHash().thenComposeAsync(packagesHash -> {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
try {
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) {
|
||||
verifyChecksum(getHiperLocalDirectory().resolve("hiper.exe"), "SHA-1", packagesHash.get(String.format("%s/hiper.exe", HIPER_TARGET_NAME)));
|
||||
verifyChecksum(getHiperLocalDirectory().resolve("wintun.dll"), "SHA-1", 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"));
|
||||
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)));
|
||||
}
|
||||
|
||||
future.complete(null);
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to verify HiPer files", e);
|
||||
Platform.runLater(() -> Controllers.taskDialog(MultiplayerManager.downloadHiper()
|
||||
.whenComplete(exception -> {
|
||||
if (exception == null)
|
||||
future.complete(null);
|
||||
else
|
||||
future.completeExceptionally(exception);
|
||||
}), i18n("multiplayer.download"), TaskCancellationAction.NORMAL));
|
||||
}
|
||||
return future;
|
||||
}).thenApplyAsync(wrap(ignored -> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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("binary");
|
||||
}
|
||||
|
||||
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) {
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
synchronized (HIPER_VALID_TIME_FORMAT) {
|
||||
Date date = HIPER_VALID_TIME_FORMAT.parse(validUntil.get());
|
||||
onValidUntil.fireEvent(new HiperShowValidUntilEvent(this, date));
|
||||
}
|
||||
} catch (JsonParseException | ParseException 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();
|
||||
}
|
||||
|
||||
@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() {
|
||||
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 final int NO_SUDO_PRIVILEGES = -6;
|
||||
}
|
||||
|
||||
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 HiperInvalidTokenException extends RuntimeException {
|
||||
}
|
||||
|
||||
public static class HiperUnsupportedPlatformException extends RuntimeException {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +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.auth.authlibinjector.AuthlibInjectorArtifactProvider;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.auth.offline.Skin;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class MultiplayerOfflineAccount extends OfflineAccount {
|
||||
|
||||
public MultiplayerOfflineAccount(AuthlibInjectorArtifactProvider downloader, String username, UUID uuid, Skin skin) {
|
||||
super(downloader, username, uuid, skin);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean loadAuthlibInjector(Skin skin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,367 +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.auth.Account;
|
||||
import org.jackhuang.hmcl.auth.offline.OfflineAccount;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.*;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.versions.Versions;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.TaskCancellationAction;
|
||||
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.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();
|
||||
}
|
||||
|
||||
void launchGame() {
|
||||
Profile profile = Profiles.getSelectedProfile();
|
||||
Versions.launch(profile, profile.getSelectedVersion(), (launcherHelper) -> {
|
||||
launcherHelper.setKeep();
|
||||
Account account = launcherHelper.getAccount();
|
||||
if (account instanceof OfflineAccount && !(account instanceof MultiplayerOfflineAccount)) {
|
||||
OfflineAccount offlineAccount = (OfflineAccount) account;
|
||||
launcherHelper.setAccount(new MultiplayerOfflineAccount(
|
||||
offlineAccount.getDownloader(),
|
||||
offlineAccount.getUsername(),
|
||||
offlineAccount.getUUID(),
|
||||
offlineAccount.getSkin()
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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.warning("HiPer invalid configuration");
|
||||
return i18n("multiplayer.token.malformed");
|
||||
} else if (e instanceof ChecksumMismatchException) {
|
||||
LOG.log(Level.WARNING, "Failed to verify HiPer files", e);
|
||||
return i18n("multiplayer.error.file_not_found");
|
||||
} else if (e instanceof MultiplayerManager.HiperExitException) {
|
||||
int exitCode = ((MultiplayerManager.HiperExitException) e).getExitCode();
|
||||
LOG.warning("HiPer exited unexpectedly with exit code " + exitCode);
|
||||
return i18n("multiplayer.exit", exitCode);
|
||||
} else if (e instanceof MultiplayerManager.HiperInvalidTokenException) {
|
||||
LOG.warning("invalid token");
|
||||
return i18n("multiplayer.token.invalid");
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Unknown HiPer exception", e);
|
||||
return e.getLocalizedMessage() + "\n" + StringUtils.getStackTrace(e);
|
||||
}
|
||||
}
|
||||
|
||||
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.expireTime.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.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:
|
||||
// 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,461 +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.application.Platform;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.WeakInvalidationListener;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
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.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
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.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
|
||||
import org.jackhuang.hmcl.util.HMCLService;
|
||||
import org.jackhuang.hmcl.util.Lang;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
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.ui.versions.VersionPage.wrap;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
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 -> {
|
||||
control.launchGame();
|
||||
});
|
||||
})
|
||||
.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(item -> {
|
||||
// item.setTitle(i18n("multiplayer.help.1"));
|
||||
// item.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
// item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/admin.html"));
|
||||
// })
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("multiplayer.help.2"));
|
||||
item.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/help.html"));
|
||||
})
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("multiplayer.help.3"));
|
||||
item.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/help.html#%E5%88%9B%E5%BB%BA%E6%96%B9"));
|
||||
})
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("multiplayer.help.4"));
|
||||
item.setLeftGraphic(wrap(SVG::helpCircleOutline));
|
||||
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/help.html#%E5%8F%82%E4%B8%8E%E8%80%85"));
|
||||
})
|
||||
.addNavigationDrawerItem(item -> {
|
||||
item.setTitle(i18n("multiplayer.help.text"));
|
||||
item.setLeftGraphic(wrap(SVG::rocketLaunchOutline));
|
||||
item.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/multiplayer/text.html"));
|
||||
})
|
||||
.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(MessageType.WARNING);
|
||||
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"));
|
||||
|
||||
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"));
|
||||
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());
|
||||
startButton.disableProperty().bind(MultiplayerManager.tokenInvalid);
|
||||
|
||||
startPane.getChildren().setAll(startButton);
|
||||
startPane.setAlignment(Pos.CENTER_RIGHT);
|
||||
}
|
||||
|
||||
if (!MultiplayerManager.IS_ADMINISTRATOR)
|
||||
offPane.getContent().add(hintPane);
|
||||
offPane.getContent().addAll(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.setRight(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(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(MessageType.INFO);
|
||||
GridPane.setColumnSpan(hintPane, 3);
|
||||
hintPane.setText(i18n("multiplayer.slave.hint"));
|
||||
slavePane.getChildren().add(hintPane);
|
||||
|
||||
HintPane hintPane2 = new HintPane(MessageType.WARNING);
|
||||
GridPane.setColumnSpan(hintPane2, 3);
|
||||
hintPane2.setText(i18n("multiplayer.slave.hint2"));
|
||||
slavePane.getChildren().add(hintPane2);
|
||||
|
||||
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, hintPane2, notBroadcastingPane);
|
||||
} else {
|
||||
slavePane.getChildren().setAll(titlePane, hintPane, hintPane2, 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(offPane);
|
||||
} else {
|
||||
mainPane.getChildren().setAll(onPane);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
{
|
||||
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("multiplayer.persistence")),
|
||||
persistencePane,
|
||||
ComponentList.createComponentListTitle(i18n("about")),
|
||||
thanksPane
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user