Remove HiPer Support

This commit is contained in:
Glavo
2022-11-11 18:09:17 +08:00
committed by Haowei Wen
parent eaddb67932
commit 53336aedc9
10 changed files with 8 additions and 1616 deletions

View File

@@ -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",

View File

@@ -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));

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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 {
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
);
}
}
}