diff options
author | Aviana Cruz | 2022-11-06 15:45:20 +0800 |
---|---|---|
committer | Aviana Cruz | 2022-11-06 15:45:20 +0800 |
commit | 79d5654da5dfbda9220e5ce71cac96275d2f874a (patch) | |
tree | 2cad1de3119c5fc5e21a5011d34710b7c99fc2d5 | |
parent | 99fa14cf8c8c5c9bed11c596c14cbed2d70c6387 (diff) | |
download | aur-79d5654da5dfbda9220e5ce71cac96275d2f874a.tar.gz |
update 3.5.3.227 and build from source
-rw-r--r-- | .SRCINFO | 19 | ||||
-rw-r--r-- | 0001-Target-java-17.patch | 42 | ||||
-rw-r--r-- | 0002-Cleanup.patch | 3837 | ||||
-rw-r--r-- | PKGBUILD | 50 | ||||
-rwxr-xr-x | hmcl-launch-script | 11 |
5 files changed, 3936 insertions, 23 deletions
@@ -1,23 +1,26 @@ pkgbase = hmcl-new pkgdesc = An unofficial build of HMCL that trying to compile and run HMCL with the latest LTS version of java. - pkgver = 3.5.3.223 + pkgver = 3.5.3.227 pkgrel = 1 - url = https://github.com/skbeh/HMCL-build + url = https://github.com/huanghongxun/HMCL arch = any license = GPL3 + makedepends = java-environment>=17 + makedepends = gradle depends = java-openjfx>=17 provides = hmcl conflicts = hmcl - noextract = hmcl-new-3.5.3.223-1.jar source = hmcl.desktop source = hmcl-launch-script source = craft_table.png - source = LICENSE::https://raw.githubusercontent.com/huanghongxun/HMCL/javafx/LICENSE - source = hmcl-new-3.5.3.223-1.jar::https://github.com/skbeh/HMCL-build/releases/download/v3.5.3.223/HMCL-3.5.3.223.jar + source = hmcl-new-3.5.3.227.tar.gz::https://github.com/huanghongxun/HMCL/archive/refs/tags/v3.5.3.227.tar.gz + source = 0001-Target-java-17.patch + source = 0002-Cleanup.patch sha256sums = b4e8aa0f349bb3f5dd15a31c5a13ac3e10e5a5bcd2f97cf390041924275e43ef - sha256sums = 534e391a637394e47cdeb0d9dfe24cd6fd1dedb863c085951403ec24f1470d06 + sha256sums = 9adb4243a5123ff82cb3678ebb3e889250d745973859d57ab5a14b2867b7cb04 sha256sums = 2989a1b5301b8c7b9afdae5696c6a4e5246afa2d4f1f3d3dad5c192f036a9b4c - sha256sums = 3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986 - sha256sums = d3680eb1f126f1d9cc2239bf68387165ce2d91b3cc9cd510a8339dddb68fb94d + sha256sums = 6d0b1fa5d4a7cab1024e62c12ea6baf19197175e2d2d3af9b23099878057b92f + sha256sums = 6348216b7c7c9b4d44355d19e11ea6d27d7b1d48d3f0a43079ab929e70728448 + sha256sums = 34b2d477abed1858dc36069fc4374510f9ef52632fd415e0a077f99240ee20e6 pkgname = hmcl-new diff --git a/0001-Target-java-17.patch b/0001-Target-java-17.patch new file mode 100644 index 000000000000..ef6879350318 --- /dev/null +++ b/0001-Target-java-17.patch @@ -0,0 +1,42 @@ +From dcfa33dbd0627171aa7cbe1fc51aa61f90883bda Mon Sep 17 00:00:00 2001 +From: skbeh <60107333+skbeh@users.noreply.github.com> +Date: Sun, 6 Nov 2022 13:28:59 +0800 +Subject: [PATCH 1/2] Target java 17 + +Signed-off-by: Aviana Cruz <gwencroft@proton.me> +--- + HMCL/build.gradle.kts | 2 +- + build.gradle.kts | 4 ++-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts +index 90756b10..6f5c6e14 100644 +--- a/HMCL/build.gradle.kts ++++ b/HMCL/build.gradle.kts +@@ -28,7 +28,7 @@ buildscript { + } + + plugins { +- id("com.github.johnrengelman.shadow") version "7.0.0" ++ id("com.github.johnrengelman.shadow") version "7.1.2" + } + + val buildNumber = System.getenv("BUILD_NUMBER")?.toInt().let { number -> +diff --git a/build.gradle.kts b/build.gradle.kts +index 8d6a9ce2..674cf430 100644 +--- a/build.gradle.kts ++++ b/build.gradle.kts +@@ -23,8 +23,8 @@ subprojects { + } + + tasks.withType<JavaCompile> { +- sourceCompatibility = "1.8" +- targetCompatibility = "1.8" ++ sourceCompatibility = "17" ++ targetCompatibility = "17" + + options.encoding = "UTF-8" + } +-- +2.38.1 + diff --git a/0002-Cleanup.patch b/0002-Cleanup.patch new file mode 100644 index 000000000000..05a6db750e96 --- /dev/null +++ b/0002-Cleanup.patch @@ -0,0 +1,3837 @@ +From 4a52fdf4625a8c96042ba17ef6b922ba34dc59c0 Mon Sep 17 00:00:00 2001 +From: Aviana Cruz <gwencroft@proton.me> +Date: Sun, 6 Nov 2022 13:33:14 +0800 +Subject: [PATCH 2/2] Cleanup + +Co-authored-by: zhaose <weiliang1503@outlook.com> +Signed-off-by: Aviana Cruz <gwencroft@proton.me> +--- + .../java/org/jackhuang/hmcl/Launcher.java | 8 - + .../java/org/jackhuang/hmcl/Metadata.java | 1 - + .../jackhuang/hmcl/setting/ConfigHolder.java | 1 - + .../org/jackhuang/hmcl/ui/Controllers.java | 11 - + .../org/jackhuang/hmcl/ui/CrashWindow.java | 6 +- + .../org/jackhuang/hmcl/ui/UpgradeDialog.java | 77 --- + .../jackhuang/hmcl/ui/main/FeedbackPage.java | 471 --------------- + .../hmcl/ui/main/LauncherSettingsPage.java | 22 +- + .../org/jackhuang/hmcl/ui/main/MainPage.java | 117 +--- + .../org/jackhuang/hmcl/ui/main/RootPage.java | 22 - + .../jackhuang/hmcl/ui/main/SettingsPage.java | 60 -- + .../jackhuang/hmcl/ui/main/SettingsView.java | 58 -- + .../jackhuang/hmcl/ui/main/SponsorPage.java | 166 ------ + .../multiplayer/LocalServerBroadcaster.java | 153 ----- + .../ui/multiplayer/MultiplayerManager.java | 553 ------------------ + .../hmcl/ui/multiplayer/MultiplayerPage.java | 367 ------------ + .../ui/multiplayer/MultiplayerPageSkin.java | 461 --------------- + .../hmcl/upgrade/ExecutableHeaderHelper.java | 123 ---- + .../hmcl/upgrade/HMCLDownloadTask.java | 68 --- + .../hmcl/upgrade/IntegrityChecker.java | 134 ----- + .../jackhuang/hmcl/upgrade/RemoteVersion.java | 96 --- + .../jackhuang/hmcl/upgrade/UpdateChannel.java | 42 -- + .../jackhuang/hmcl/upgrade/UpdateChecker.java | 125 ---- + .../jackhuang/hmcl/upgrade/UpdateHandler.java | 257 -------- + .../jackhuang/hmcl/util/CrashReporter.java | 5 - + 25 files changed, 3 insertions(+), 3401 deletions(-) + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java + delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java + +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +index bcd49e1d..0d851357 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java +@@ -30,8 +30,6 @@ import org.jackhuang.hmcl.task.AsyncTaskExecutor; + import org.jackhuang.hmcl.task.Schedulers; + import org.jackhuang.hmcl.ui.AwtUtils; + import org.jackhuang.hmcl.ui.Controllers; +-import org.jackhuang.hmcl.upgrade.UpdateChecker; +-import org.jackhuang.hmcl.upgrade.UpdateHandler; + import org.jackhuang.hmcl.util.CrashReporter; + import org.jackhuang.hmcl.util.Lang; + import org.jackhuang.hmcl.util.StringUtils; +@@ -133,8 +131,6 @@ public final class Launcher extends Application { + + initIcon(); + +- UpdateChecker.init(); +- + primaryStage.show(); + }); + } catch (Throwable e) { +@@ -155,10 +151,6 @@ public final class Launcher extends Application { + } + + public static void main(String[] args) { +- if (UpdateHandler.processArguments(args)) { +- return; +- } +- + Thread.setDefaultUncaughtExceptionHandler(CRASH_REPORTER); + AsyncTaskExecutor.setUncaughtExceptionHandler(new CrashReporter(false)); + +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +index f24a2d47..7f49a55f 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +@@ -35,7 +35,6 @@ public final class Metadata { + public static final String TITLE = NAME + " " + VERSION; + public static final String FULL_TITLE = FULL_NAME + " v" + VERSION; + +- public static final String UPDATE_URL = System.getProperty("hmcl.update_source.override", "https://hmcl.huangyuhui.net/api/update_link"); + public static final String CONTACT_URL = "https://github.com/huanghongxun/HMCL/issues"; + public static final String HELP_URL = "https://hmcl.huangyuhui.net/help"; + public static final String CHANGELOG_URL = "https://docs.hmcl.net/changelog/"; +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java +index 17342bcd..f7572c81 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/ConfigHolder.java +@@ -169,7 +169,6 @@ public final class ConfigHolder { + LOG.info("Config is empty"); + } else { + Map<?, ?> raw = new Gson().fromJson(content, Map.class); +- ConfigUpgrader.upgradeConfig(deserialized, raw); + return deserialized; + } + } catch (JsonParseException e) { +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +index ba598ac5..47330c98 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +@@ -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(); +@@ -305,10 +298,6 @@ public final class Controllers { + + public static void onHyperlinkAction(String href) { + if (href.startsWith("hmcl://")) { +- if ("hmcl://settings/feedback".equals(href)) { +- Controllers.getSettingsPage().showFeedback(); +- Controllers.navigate(Controllers.getSettingsPage()); +- } + } else { + FXUtils.openLink(href); + } +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +index 46ce19ab..daafd6f6 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/CrashWindow.java +@@ -27,7 +27,6 @@ import javafx.scene.layout.HBox; + import javafx.scene.layout.StackPane; + import javafx.stage.Stage; + import org.jackhuang.hmcl.Metadata; +-import org.jackhuang.hmcl.upgrade.UpdateChecker; + + import static org.jackhuang.hmcl.ui.FXUtils.newImage; + import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +@@ -39,10 +38,7 @@ public class CrashWindow extends Stage { + + public CrashWindow(String text) { + Label lblCrash = new Label(); +- if (UpdateChecker.isOutdated()) +- lblCrash.setText(i18n("launcher.crash_out_dated")); +- else +- lblCrash.setText(i18n("launcher.crash")); ++ lblCrash.setText(i18n("launcher.crash")); + lblCrash.setWrapText(true); + + TextArea textArea = new TextArea(); +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java +deleted file mode 100644 +index 0063690c..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/UpgradeDialog.java ++++ /dev/null +@@ -1,77 +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; +- +-import com.jfoenix.controls.JFXButton; +-import com.jfoenix.controls.JFXDialogLayout; +-import javafx.concurrent.Worker; +-import javafx.scene.control.Label; +-import javafx.scene.web.WebEngine; +-import javafx.scene.web.WebView; +-import org.jackhuang.hmcl.Metadata; +-import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +-import org.jackhuang.hmcl.upgrade.RemoteVersion; +- +-import java.util.logging.Level; +- +-import static org.jackhuang.hmcl.Metadata.CHANGELOG_URL; +-import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +-import static org.jackhuang.hmcl.util.Logging.LOG; +-import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +- +-public class UpgradeDialog extends JFXDialogLayout { +- public UpgradeDialog(RemoteVersion remoteVersion, Runnable updateRunnable) { +- { +- setHeading(new Label(i18n("update.changelog"))); +- } +- +- { +- String url = CHANGELOG_URL + remoteVersion.getChannel().channelName + ".html"; +- try { +- WebView webView = new WebView(); +- webView.getEngine().setUserDataDirectory(Metadata.HMCL_DIRECTORY.toFile()); +- WebEngine engine = webView.getEngine(); +- engine.load(url); +- engine.getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> { +- if (newValue == Worker.State.FAILED) { +- LOG.warning("Failed to load update log, trying to open it in browser"); +- FXUtils.openLink(url); +- setBody(); +- } +- }); +- setBody(webView); +- } catch (NoClassDefFoundError | UnsatisfiedLinkError e) { +- LOG.log(Level.WARNING, "WebView is missing or initialization failed", e); +- FXUtils.openLink(url); +- } +- } +- +- { +- JFXButton updateButton = new JFXButton(i18n("update.accept")); +- updateButton.getStyleClass().add("dialog-accept"); +- updateButton.setOnMouseClicked(e -> updateRunnable.run()); +- +- JFXButton cancelButton = new JFXButton(i18n("button.cancel")); +- cancelButton.getStyleClass().add("dialog-cancel"); +- cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent())); +- +- setActions(updateButton, cancelButton); +- onEscPressed(this, cancelButton::fire); +- } +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java +deleted file mode 100644 +index f67736d5..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java ++++ /dev/null +@@ -1,471 +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.main; +- +-import com.google.gson.JsonParseException; +-import com.google.gson.annotations.SerializedName; +-import com.google.gson.reflect.TypeToken; +-import com.jfoenix.controls.*; +-import javafx.beans.binding.Bindings; +-import javafx.beans.property.BooleanProperty; +-import javafx.beans.property.SimpleBooleanProperty; +-import javafx.collections.FXCollections; +-import javafx.collections.ObservableList; +-import javafx.geometry.Insets; +-import javafx.geometry.Pos; +-import javafx.scene.control.Label; +-import javafx.scene.layout.*; +-import org.jackhuang.hmcl.Metadata; +-import org.jackhuang.hmcl.game.OAuthServer; +-import org.jackhuang.hmcl.setting.Accounts; +-import org.jackhuang.hmcl.setting.HMCLAccounts; +-import org.jackhuang.hmcl.setting.Theme; +-import org.jackhuang.hmcl.task.Schedulers; +-import org.jackhuang.hmcl.task.Task; +-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.util.StringUtils; +-import org.jackhuang.hmcl.util.io.HttpRequest; +-import org.jackhuang.hmcl.util.io.NetworkUtils; +-import org.jackhuang.hmcl.util.io.ResponseCodeException; +-import org.jackhuang.hmcl.util.javafx.BindingMapping; +- +-import java.io.IOException; +-import java.net.HttpURLConnection; +-import java.util.List; +-import java.util.Locale; +-import java.util.Map; +- +-import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +-import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; +-import static org.jackhuang.hmcl.util.Lang.mapOf; +-import static org.jackhuang.hmcl.util.Pair.pair; +-import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +- +-public class FeedbackPage extends VBox implements PageAware { +- private final ObservableList<FeedbackResponse> feedbacks = FXCollections.observableArrayList(); +- private final SpinnerPane spinnerPane = new SpinnerPane(); +- +- public FeedbackPage() { +- setSpacing(10); +- setPadding(new Insets(10)); +- +- { +- HBox loginPane = new HBox(16); +- loginPane.setAlignment(Pos.CENTER_LEFT); +- loginPane.getStyleClass().add("card"); +- +- TwoLineListItem accountInfo = new TwoLineListItem(); +- HBox.setHgrow(accountInfo, Priority.ALWAYS); +- accountInfo.titleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) +- .map(account -> account == null ? i18n("account.not_logged_in") : account.getNickname())); +- accountInfo.subtitleProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) +- .map(account -> account == null ? i18n("account.not_logged_in") : account.getEmail())); +- +- JFXButton logButton = new JFXButton(); +- logButton.textProperty().bind(BindingMapping.of(HMCLAccounts.accountProperty()) +- .map(account -> account == null ? i18n("account.login") : i18n("account.logout"))); +- logButton.setOnAction(e -> log()); +- +- loginPane.getChildren().setAll(accountInfo, logButton); +- getChildren().add(loginPane); +- } +- +- { +- HBox searchPane = new HBox(8); +- searchPane.getStyleClass().add("card"); +- getChildren().add(searchPane); +- +- JFXTextField searchField = new JFXTextField(); +- searchField.setOnAction(e -> search(searchField.getText(), "time", true)); +- HBox.setHgrow(searchField, Priority.ALWAYS); +- searchField.setPromptText(i18n("search")); +- +- JFXButton searchButton = new JFXButton(); +- searchButton.getStyleClass().add("toggle-icon4"); +- searchButton.setGraphic(SVG.magnify(Theme.blackFillBinding(), -1, -1)); +- searchButton.setOnAction(e -> search(searchField.getText(), "time", true)); +- +- JFXButton addButton = new JFXButton(); +- addButton.getStyleClass().add("toggle-icon4"); +- addButton.setGraphic(SVG.plus(Theme.blackFillBinding(), -1, -1)); +- addButton.setOnAction(e -> addFeedback()); +- +- searchPane.getChildren().setAll(searchField, searchButton, addButton); +- } +- +- { +- spinnerPane.getStyleClass().add("card"); +- VBox.setVgrow(spinnerPane, Priority.ALWAYS); +- JFXListView<FeedbackResponse> listView = new JFXListView<>(); +- spinnerPane.setContent(listView); +- Bindings.bindContent(listView.getItems(), feedbacks); +- listView.setCellFactory(x -> new MDListCell<FeedbackResponse>(listView) { +- private final TwoLineListItem content = new TwoLineListItem(); +- private final JFXButton likeButton = new JFXButton(); +- private final JFXButton unlikeButton = new JFXButton(); +- private final HBox container; +- +- { +- container = new HBox(8); +- container.setPickOnBounds(false); +- container.setAlignment(Pos.CENTER_LEFT); +- HBox.setHgrow(content, Priority.ALWAYS); +- content.setMouseTransparent(false); +- setSelectable(); +- +- likeButton.getStyleClass().add("toggle-icon4"); +- likeButton.setGraphic(FXUtils.limitingSize(SVG.thumbUpOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); +- +- unlikeButton.getStyleClass().add("toggle-icon4"); +- unlikeButton.setGraphic(FXUtils.limitingSize(SVG.thumbDownOutline(Theme.blackFillBinding(), 24, 24), 24, 24)); +- +- container.getChildren().setAll(content, likeButton, unlikeButton); +- +- StackPane.setMargin(container, new Insets(10, 16, 10, 16)); +- getContainer().getChildren().setAll(container); +- } +- +- @Override +- protected void updateControl(FeedbackResponse feedback, boolean empty) { +- if (empty) return; +- content.setTitle(feedback.getTitle()); +- content.setSubtitle(feedback.getAuthor()); +- content.getTags().setAll( +- "#" + feedback.getId(), +- i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US)), +- i18n("feedback.type." + feedback.getType().name().toLowerCase(Locale.US))); +- content.setOnMouseClicked(e -> { +- getFeedback(feedback.getId()) +- .thenAcceptAsync(Schedulers.javafx(), f -> { +- Controllers.dialog(new ViewFeedbackDialog(f)); +- }) +- .start(); +- }); +- } +- }); +- +- getChildren().add(spinnerPane); +- } +- } +- +- @Override +- public void onPageShown() { +- search("", "time", false); +- } +- +- private void search(String keyword, String order, boolean showAll) { +- HMCLAccounts.HMCLAccount account = HMCLAccounts.getAccount(); +- Task.supplyAsync(() -> { +- Map<String, String> query = mapOf( +- pair("keyword", keyword), +- pair("order", order) +- ); +- if (showAll) { +- query.put("showAll", "1"); +- } +- HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query)); +- if (account != null) { +- req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) +- .header("Authorization-Provider", HMCLAccounts.getAccount().getProvider()); +- } +- return req.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType()); +- }).whenComplete(Schedulers.javafx(), (result, exception) -> { +- spinnerPane.hideSpinner(); +- if (exception != null) { +- if (exception instanceof ResponseCodeException) { +- int responseCode = ((ResponseCodeException) exception).getResponseCode(); +- if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { +- spinnerPane.setFailedReason(i18n("feedback.failed.permission")); +- return; +- } else if (responseCode == 429) { +- spinnerPane.setFailedReason(i18n("feedback.failed.too_frequently")); +- return; +- } +- } +- spinnerPane.setFailedReason(i18n("feedback.failed")); +- } else { +- feedbacks.setAll(result); +- } +- }).start(); +- } +- +- private Task<FeedbackResponse> getFeedback(int id) { +- return Task.supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/feedback/" + id).getJson(FeedbackResponse.class)); +- } +- +- private void log() { +- if (HMCLAccounts.getAccount() == null) { +- // login +- Controllers.dialog(new LoginDialog()); +- } else { +- // logout +- HMCLAccounts.setAccount(null); +- } +- } +- +- private void addFeedback() { +- if (HMCLAccounts.getAccount() == null) { +- Controllers.dialog(i18n("feedback.add.login")); +- return; +- } +- +- Controllers.dialog(new AddFeedbackDialog()); +- } +- +- private class LoginDialog extends JFXDialogLayout { +- private final SpinnerPane spinnerPane = new SpinnerPane(); +- private final Label errorLabel = new Label(); +- private final BooleanProperty logging = new SimpleBooleanProperty(); +- +- public LoginDialog() { +- setHeading(new Label(i18n("feedback.login"))); +- +- VBox vbox = new VBox(8); +- setBody(vbox); +- HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO); +- hintPane.textProperty().bind(BindingMapping.of(logging).map(logging -> +- logging +- ? i18n("account.hmcl.hint") +- : i18n("account.hmcl.hint"))); +- hintPane.setOnMouseClicked(e -> { +- if (logging.get() && OAuthServer.lastlyOpenedURL != null) { +- FXUtils.copyText(OAuthServer.lastlyOpenedURL); +- } +- }); +- vbox.getChildren().setAll(hintPane); +- +- JFXButton loginButton = new JFXButton(); +- spinnerPane.setContent(loginButton); +- loginButton.setText(i18n("account.login")); +- loginButton.setOnAction(e -> login()); +- +- JFXButton cancelButton = new JFXButton(); +- cancelButton.setText(i18n("button.cancel")); +- cancelButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); +- onEscPressed(this, cancelButton::fire); +- +- setActions(errorLabel, spinnerPane, cancelButton); +- } +- +- private void login() { +- spinnerPane.showSpinner(); +- errorLabel.setText(""); +- logging.set(true); +- +- HMCLAccounts.login().whenComplete(Schedulers.javafx(), (result, exception) -> { +- logging.set(false); +- if (exception != null) { +- if (exception instanceof IOException) { +- errorLabel.setText(i18n("account.failed.connect_authentication_server")); +- } else if (exception instanceof JsonParseException) { +- errorLabel.setText(i18n("account.failed.server_response_malformed")); +- } else { +- errorLabel.setText(Accounts.localizeErrorMessage(exception)); +- } +- } else { +- fireEvent(new DialogCloseEvent()); +- } +- }).start(); +- } +- } +- +- private static class AddFeedbackDialog extends DialogPane { +- +- JFXTextField titleField = new JFXTextField(); +- JFXComboBox<FeedbackType> comboBox = new JFXComboBox<>(); +- JFXTextArea contentArea = new JFXTextArea(); +- +- public AddFeedbackDialog() { +- setTitle(i18n("feedback.add")); +- +- GridPane body = new GridPane(); +- body.setVgap(8); +- body.setHgap(8); +- +- HintPane searchHintPane = new HintPane(MessageDialogPane.MessageType.WARNING); +- GridPane.setColumnSpan(searchHintPane, 2); +- searchHintPane.setText(i18n("feedback.add.hint.search_before_add")); +- body.addRow(0, searchHintPane); +- +- HintPane titleHintPane = new HintPane(MessageDialogPane.MessageType.INFO); +- GridPane.setColumnSpan(titleHintPane, 2); +- titleHintPane.setText(i18n("feedback.add.hint.title")); +- body.addRow(1, titleHintPane); +- +- titleField.setValidators(new RequiredValidator()); +- body.addRow(2, new Label(i18n("feedback.title")), titleField); +- +- comboBox.setMaxWidth(-1); +- comboBox.getItems().setAll(FeedbackType.values()); +- comboBox.getSelectionModel().select(0); +- comboBox.setConverter(stringConverter(e -> i18n("feedback.type." + e.name().toLowerCase()))); +- body.addRow(3, new Label(i18n("feedback.type")), comboBox); +- +- Label contentLabel = new Label(i18n("feedback.content")); +- GridPane.setColumnSpan(contentLabel, 2); +- body.addRow(4, contentLabel); +- +- contentArea.setValidators(new RequiredValidator()); +- contentArea.setPromptText(i18n("feedback.add.hint.content")); +- GridPane.setColumnSpan(contentArea, 2); +- body.addRow(5, contentArea); +- +- validProperty().bind(Bindings.createBooleanBinding(() -> { +- return titleField.validate() && contentArea.validate(); +- }, titleField.textProperty(), contentArea.textProperty())); +- +- setBody(body); +- } +- +- @Override +- protected void onAccept() { +- setLoading(); +- +- addFeedback(titleField.getText(), comboBox.getValue(), contentArea.getText()) +- .whenComplete(Schedulers.javafx(), exception -> { +- if (exception != null) { +- onFailure(exception.getLocalizedMessage()); +- } else { +- onSuccess(); +- } +- }) +- .start(); +- } +- +- private Task<?> addFeedback(String title, FeedbackType feedbackType, String content) { +- return Task.runAsync(() -> { +- HttpRequest.POST("https://hmcl.huangyuhui.net/api/feedback") +- .json(mapOf( +- pair("title", title), +- pair("content", content), +- pair("type", feedbackType.name().toLowerCase(Locale.ROOT)), +- pair("launcher_version", Metadata.VERSION) +- )) +- .authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) +- .header("Authorization-Provider", HMCLAccounts.getAccount().getProvider()) +- .getString(); +- }); +- } +- } +- +- private static class ViewFeedbackDialog extends JFXDialogLayout { +- +- public ViewFeedbackDialog(FeedbackResponse feedback) { +- BorderPane heading = new BorderPane(); +- TwoLineListItem left = new TwoLineListItem(); +- heading.setLeft(left); +- left.setTitle(feedback.getTitle()); +- left.setSubtitle(feedback.getAuthor()); +- left.getTags().add("#" + feedback.getId()); +- left.getTags().add(i18n("feedback.state." + feedback.getState().name().toLowerCase(Locale.US))); +- left.getTags().add(feedback.getLauncherVersion()); +- left.getTags().add(i18n("feedback.type." + feedback.getType().name().toLowerCase())); +- +- setHeading(heading); +- +- Label content = new Label(feedback.getContent()); +- content.setWrapText(true); +- +- TwoLineListItem response = new TwoLineListItem(); +- response.getStyleClass().setAll("two-line-item-second-large"); +- response.setTitle(i18n("feedback.response")); +- response.setSubtitle(StringUtils.isBlank(feedback.getReason()) +- ? i18n("feedback.response.empty") +- : feedback.getReason()); +- +- VBox body = new VBox(content, response); +- body.setSpacing(8); +- setBody(body); +- +- JFXButton okButton = new JFXButton(); +- okButton.setText(i18n("button.ok")); +- okButton.setOnAction(e -> fireEvent(new DialogCloseEvent())); +- +- setActions(okButton); +- } +- } +- +- private static class FeedbackResponse { +- private final int id; +- private final String title; +- private final String content; +- private final String author; +- @SerializedName("launcher_version") +- private final String launcherVersion; +- private final FeedbackType type; +- private final FeedbackState state; +- private final String reason; +- +- public FeedbackResponse(int id, String title, String content, String author, String launcherVersion, FeedbackType type, FeedbackState state, String reason) { +- this.id = id; +- this.title = title; +- this.content = content; +- this.author = author; +- this.launcherVersion = launcherVersion; +- this.type = type; +- this.state = state; +- this.reason = reason; +- } +- +- public int getId() { +- return id; +- } +- +- public String getTitle() { +- return title; +- } +- +- public String getContent() { +- return content; +- } +- +- public String getAuthor() { +- return author; +- } +- +- public String getLauncherVersion() { +- return launcherVersion; +- } +- +- public FeedbackType getType() { +- return type; +- } +- +- public FeedbackState getState() { +- return state; +- } +- +- public String getReason() { +- return reason; +- } +- } +- +- private enum FeedbackType { +- FEATURE, +- BUG +- } +- +- private enum FeedbackState { +- OPEN, +- REJECTED, +- ACCEPTED +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +index d6c11595..d7586282 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +@@ -44,8 +44,6 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor + private final TabHeader.Tab<DownloadSettingsPage> downloadTab = new TabHeader.Tab<>("downloadSettingsPage"); + private final TabHeader.Tab<HelpPage> helpTab = new TabHeader.Tab<>("helpPage"); + private final TabHeader.Tab<AboutPage> aboutTab = new TabHeader.Tab<>("aboutPage"); +- private final TabHeader.Tab<FeedbackPage> feedbackTab = new TabHeader.Tab<>("feedbackPage"); +- private final TabHeader.Tab<SponsorPage> sponsorTab = new TabHeader.Tab<>("sponsorPage"); + private final TransitionPane transitionPane = new TransitionPane(); + + public LauncherSettingsPage() { +@@ -54,10 +52,8 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor + personalizationTab.setNodeSupplier(PersonalizationPage::new); + downloadTab.setNodeSupplier(DownloadSettingsPage::new); + helpTab.setNodeSupplier(HelpPage::new); +- feedbackTab.setNodeSupplier(FeedbackPage::new); +- sponsorTab.setNodeSupplier(SponsorPage::new); + aboutTab.setNodeSupplier(AboutPage::new); +- tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, sponsorTab, aboutTab); ++ tab = new TabHeader(gameTab, settingsTab, personalizationTab, downloadTab, helpTab, aboutTab); + + tab.select(gameTab); + gameTab.initializeIfNeeded(); +@@ -100,18 +96,6 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor + helpItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(helpTab)); + helpItem.setOnAction(e -> tab.select(helpTab)); + }) +- .addNavigationDrawerItem(feedbackItem -> { +- feedbackItem.setTitle(i18n("feedback")); +- feedbackItem.setLeftGraphic(wrap(SVG::messageAlertOutline)); +- feedbackItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(feedbackTab)); +- feedbackItem.setOnAction(e -> tab.select(feedbackTab)); +- }) +- .addNavigationDrawerItem(sponsorItem -> { +- sponsorItem.setTitle(i18n("sponsor")); +- sponsorItem.setLeftGraphic(wrap(SVG::handHearOutline)); +- sponsorItem.activeProperty().bind(tab.getSelectionModel().selectedItemProperty().isEqualTo(sponsorTab)); +- sponsorItem.setOnAction(e -> tab.select(sponsorTab)); +- }) + .addNavigationDrawerItem(aboutItem -> { + aboutItem.setTitle(i18n("about")); + aboutItem.setLeftGraphic(wrap(SVG::informationOutline)); +@@ -140,10 +124,6 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor + tab.select(gameTab); + } + +- public void showFeedback() { +- tab.select(feedbackTab); +- } +- + @Override + public ReadOnlyObjectProperty<State> stateProperty() { + return state.getReadOnlyProperty(); +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +index 3cf4ef93..1b0f9571 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +@@ -52,9 +52,6 @@ import org.jackhuang.hmcl.ui.construct.TwoLineListItem; + import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + import org.jackhuang.hmcl.ui.versions.GameItem; + import org.jackhuang.hmcl.ui.versions.Versions; +-import org.jackhuang.hmcl.upgrade.RemoteVersion; +-import org.jackhuang.hmcl.upgrade.UpdateChecker; +-import org.jackhuang.hmcl.upgrade.UpdateHandler; + import org.jackhuang.hmcl.util.javafx.BindingMapping; + import org.jackhuang.hmcl.util.javafx.MappedObservableList; + +@@ -73,14 +70,11 @@ public final class MainPage extends StackPane implements DecoratorPage { + private final JFXPopup popup = new JFXPopup(menu); + + private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame"); +- private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate"); +- private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion"); + private final ObservableList<Version> versions = FXCollections.observableArrayList(); + private final ObservableList<Node> versionNodes; + private Profile profile; + + private final VBox announcementPane; +- private final StackPane updatePane; + private final JFXButton menuButton; + + { +@@ -101,44 +95,6 @@ public final class MainPage extends StackPane implements DecoratorPage { + + announcementPane = new VBox(16); + +- updatePane = new StackPane(); +- updatePane.setVisible(false); +- updatePane.getStyleClass().add("bubble"); +- FXUtils.setLimitWidth(updatePane, 230); +- FXUtils.setLimitHeight(updatePane, 55); +- StackPane.setAlignment(updatePane, Pos.TOP_RIGHT); +- updatePane.setOnMouseClicked(e -> onUpgrade()); +- FXUtils.onChange(showUpdateProperty(), this::showUpdate); +- +- { +- HBox hBox = new HBox(); +- hBox.setSpacing(12); +- hBox.setAlignment(Pos.CENTER_LEFT); +- StackPane.setAlignment(hBox, Pos.CENTER_LEFT); +- StackPane.setMargin(hBox, new Insets(9, 12, 9, 16)); +- { +- Label lblIcon = new Label(); +- lblIcon.setGraphic(SVG.update(Theme.whiteFillBinding(), 20, 20)); +- +- TwoLineListItem prompt = new TwoLineListItem(); +- prompt.setSubtitle(i18n("update.bubble.subtitle")); +- prompt.setPickOnBounds(false); +- prompt.titleProperty().bind(BindingMapping.of(latestVersionProperty()).map(latestVersion -> +- latestVersion == null ? "" : i18n("update.bubble.title", latestVersion.getVersion()))); +- +- hBox.getChildren().setAll(lblIcon, prompt); +- } +- +- JFXButton closeUpdateButton = new JFXButton(); +- closeUpdateButton.setGraphic(SVG.close(Theme.whiteFillBinding(), 10, 10)); +- StackPane.setAlignment(closeUpdateButton, Pos.TOP_RIGHT); +- closeUpdateButton.getStyleClass().add("toggle-icon-tiny"); +- StackPane.setMargin(closeUpdateButton, new Insets(5)); +- closeUpdateButton.setOnMouseClicked(e -> closeUpdateBubble()); +- +- updatePane.getChildren().setAll(hBox, closeUpdateButton); +- } +- + StackPane launchPane = new StackPane(); + launchPane.getStyleClass().add("launch-pane"); + launchPane.setMaxWidth(230); +@@ -208,7 +164,7 @@ public final class MainPage extends StackPane implements DecoratorPage { + launchPane.getChildren().setAll(launchButton, separator, menuButton); + } + +- getChildren().setAll(announcementPane, updatePane, launchPane); ++ getChildren().setAll(announcementPane, launchPane); + + menu.setMaxHeight(365); + menu.setMaxWidth(545); +@@ -222,40 +178,6 @@ public final class MainPage extends StackPane implements DecoratorPage { + Bindings.bindContent(menu.getContent(), versionNodes); + } + +- public MainPage() { +- if (Metadata.isNightly()) { +- announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.nightly.title"), i18n("update.channel.nightly.hint"))); +- } else if (Metadata.isDev()) { +- announcementPane.getChildren().add(new AnnouncementCard(i18n("update.channel.dev.title"), i18n("update.channel.dev.hint"))); +- } +- } +- +- private void showUpdate(boolean show) { +- doAnimation(show); +- +- if (show && getLatestVersion() != null && !Objects.equals(config().getPromptedVersion(), getLatestVersion().getVersion())) { +- Controllers.dialog("", i18n("update.bubble.title", getLatestVersion().getVersion()), MessageDialogPane.MessageType.INFO, () -> { +- config().setPromptedVersion(getLatestVersion().getVersion()); +- onUpgrade(); +- }); +- } +- } +- +- private void doAnimation(boolean show) { +- Duration duration = Duration.millis(320); +- Timeline nowAnimation = new Timeline(); +- nowAnimation.getKeyFrames().addAll( +- new KeyFrame(Duration.ZERO, +- new KeyValue(updatePane.translateXProperty(), show ? 260 : 0, SINE)), +- new KeyFrame(duration, +- new KeyValue(updatePane.translateXProperty(), show ? 0 : 260, SINE))); +- if (show) nowAnimation.getKeyFrames().add( +- new KeyFrame(Duration.ZERO, e -> updatePane.setVisible(true))); +- else nowAnimation.getKeyFrames().add( +- new KeyFrame(duration, e -> updatePane.setVisible(false))); +- nowAnimation.play(); +- } +- + private void launch() { + Versions.launch(Profiles.getSelectedProfile()); + } +@@ -264,19 +186,6 @@ public final class MainPage extends StackPane implements DecoratorPage { + popup.show(menuButton, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.RIGHT, 0, -menuButton.getHeight()); + } + +- private void onUpgrade() { +- RemoteVersion target = UpdateChecker.getLatestVersion(); +- if (target == null) { +- return; +- } +- UpdateHandler.updateFrom(target); +- } +- +- private void closeUpdateBubble() { +- showUpdate.unbind(); +- showUpdate.set(false); +- } +- + @Override + public ReadOnlyObjectWrapper<State> stateProperty() { + return state; +@@ -294,30 +203,6 @@ public final class MainPage extends StackPane implements DecoratorPage { + this.currentGame.set(currentGame); + } + +- public boolean isShowUpdate() { +- return showUpdate.get(); +- } +- +- public BooleanProperty showUpdateProperty() { +- return showUpdate; +- } +- +- public void setShowUpdate(boolean showUpdate) { +- this.showUpdate.set(showUpdate); +- } +- +- public RemoteVersion getLatestVersion() { +- return latestVersion.get(); +- } +- +- public ObjectProperty<RemoteVersion> latestVersionProperty() { +- return latestVersion; +- } +- +- public void setLatestVersion(RemoteVersion latestVersion) { +- this.latestVersion.set(latestVersion); +- } +- + public void initVersions(Profile profile, List<Version> versions) { + FXUtils.checkFxUserThread(); + this.profile = profile; +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +index a2156067..94de43a7 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +@@ -41,7 +41,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage; + import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; + import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; + 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; +@@ -93,8 +92,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { + }); + + FXUtils.onChangeAndOperate(Profiles.selectedVersionProperty(), mainPage::setCurrentGame); +- mainPage.showUpdateProperty().bind(UpdateChecker.outdatedProperty()); +- mainPage.latestVersionProperty().bind(UpdateChecker.latestVersionProperty()); + + Profiles.registerVersionsListener(profile -> { + HMCLGameRepository repository = profile.getRepository(); +@@ -151,24 +148,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { + downloadItem.setTitle(i18n("download")); + downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage())); + +- // fifth item in left sidebar +- AdvancedListItem multiplayerItem = new AdvancedListItem(); +- 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())); +- } +- + // sixth item in left sidebar + AdvancedListItem launcherSettingsItem = new AdvancedListItem(); + launcherSettingsItem.setLeftGraphic(wrap(SVG::gearOutline)); +@@ -185,7 +164,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { + .add(gameItem) + .add(downloadItem) + .startCategory(i18n("settings.launcher.general").toUpperCase()) +- .add(multiplayerItem) + .add(launcherSettingsItem); + + // the root page, with the sidebar in left, navigator in center. +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +index 8e8f5390..9b58e9d5 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsPage.java +@@ -27,10 +27,6 @@ import org.jackhuang.hmcl.setting.Settings; + import org.jackhuang.hmcl.ui.Controllers; + import org.jackhuang.hmcl.ui.FXUtils; + import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; +-import org.jackhuang.hmcl.upgrade.RemoteVersion; +-import org.jackhuang.hmcl.upgrade.UpdateChannel; +-import org.jackhuang.hmcl.upgrade.UpdateChecker; +-import org.jackhuang.hmcl.upgrade.UpdateHandler; + import org.jackhuang.hmcl.util.Logging; + import org.jackhuang.hmcl.util.i18n.Locales; + import org.jackhuang.hmcl.util.io.FileUtils; +@@ -69,57 +65,6 @@ public final class SettingsPage extends SettingsView { + Bindings.createObjectBinding(() -> Optional.ofNullable(Settings.instance().getCommonDirectory()) + .orElse(i18n("launcher.cache_directory.disabled")), + config().commonDirectoryProperty(), config().commonDirTypeProperty())); +- +- // ==== Update ==== +- FXUtils.installFastTooltip(btnUpdate, i18n("update.tooltip")); +- updateListener = any -> { +- btnUpdate.setVisible(UpdateChecker.isOutdated()); +- +- if (UpdateChecker.isOutdated()) { +- lblUpdateSub.setText(i18n("update.newest_version", UpdateChecker.getLatestVersion().getVersion())); +- lblUpdateSub.getStyleClass().setAll("update-label"); +- +- lblUpdate.setText(i18n("update.found")); +- lblUpdate.getStyleClass().setAll("update-label"); +- } else if (UpdateChecker.isCheckingUpdate()) { +- lblUpdateSub.setText(i18n("update.checking")); +- lblUpdateSub.getStyleClass().setAll("subtitle-label"); +- +- lblUpdate.setText(i18n("update")); +- lblUpdate.getStyleClass().setAll(); +- } else { +- lblUpdateSub.setText(i18n("update.latest")); +- lblUpdateSub.getStyleClass().setAll("subtitle-label"); +- +- lblUpdate.setText(i18n("update")); +- lblUpdate.getStyleClass().setAll(); +- } +- }; +- UpdateChecker.latestVersionProperty().addListener(new WeakInvalidationListener(updateListener)); +- UpdateChecker.outdatedProperty().addListener(new WeakInvalidationListener(updateListener)); +- UpdateChecker.checkingUpdateProperty().addListener(new WeakInvalidationListener(updateListener)); +- updateListener.invalidated(null); +- +- ToggleGroup updateChannelGroup = new ToggleGroup(); +- chkUpdateDev.setToggleGroup(updateChannelGroup); +- chkUpdateDev.setUserData(UpdateChannel.DEVELOPMENT); +- chkUpdateStable.setToggleGroup(updateChannelGroup); +- chkUpdateStable.setUserData(UpdateChannel.STABLE); +- ObjectProperty<UpdateChannel> updateChannel = selectedItemPropertyFor(updateChannelGroup, UpdateChannel.class); +- updateChannel.set(UpdateChannel.getChannel()); +- updateChannel.addListener((a, b, newValue) -> { +- UpdateChecker.requestCheckUpdate(newValue); +- }); +- // ==== +- } +- +- @Override +- protected void onUpdate() { +- RemoteVersion target = UpdateChecker.getLatestVersion(); +- if (target == null) { +- return; +- } +- UpdateHandler.updateFrom(target); + } + + @Override +@@ -148,11 +93,6 @@ public final class SettingsPage extends SettingsView { + }); + } + +- @Override +- protected void onSponsor() { +- FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor"); +- } +- + @Override + protected void clearCacheDirectory() { + FileUtils.cleanDirectoryQuietly(new File(Settings.instance().getCommonDirectory(), "cache")); +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java +index af9a3477..44b6dfed 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SettingsView.java +@@ -49,11 +49,6 @@ public abstract class SettingsView extends StackPane { + protected final JFXComboBox<SupportedLocale> cboLanguage; + protected final MultiFileItem<EnumCommonDirectory> fileCommonLocation; + protected final ComponentSublist fileCommonLocationSublist; +- protected final Label lblUpdate; +- protected final Label lblUpdateSub; +- protected final JFXRadioButton chkUpdateStable; +- protected final JFXRadioButton chkUpdateDev; +- protected final JFXButton btnUpdate; + protected final ScrollPane scroll; + + public SettingsView() { +@@ -69,11 +64,6 @@ public abstract class SettingsView extends StackPane { + ComponentList settingsPane = new ComponentList(); + { + { +- StackPane sponsorPane = new StackPane(); +- sponsorPane.setCursor(Cursor.HAND); +- sponsorPane.setOnMouseClicked(e -> onSponsor()); +- sponsorPane.setPadding(new Insets(8, 0, 8, 0)); +- + GridPane gridPane = new GridPane(); + + ColumnConstraints col = new ColumnConstraints(); +@@ -96,51 +86,7 @@ public abstract class SettingsView extends StackPane { + GridPane.setColumnIndex(label, 0); + gridPane.getChildren().add(label); + } +- +- sponsorPane.getChildren().setAll(gridPane); +- settingsPane.getContent().add(sponsorPane); +- } +- } +- +- { +- ComponentSublist updatePane = new ComponentSublist(); +- updatePane.setTitle(i18n("update")); +- updatePane.setHasSubtitle(true); +- { +- VBox headerLeft = new VBox(); +- +- lblUpdate = new Label(i18n("update")); +- lblUpdateSub = new Label(); +- lblUpdateSub.getStyleClass().add("subtitle-label"); +- +- headerLeft.getChildren().setAll(lblUpdate, lblUpdateSub); +- updatePane.setHeaderLeft(headerLeft); +- } +- +- { +- btnUpdate = new JFXButton(); +- btnUpdate.setOnMouseClicked(e -> onUpdate()); +- btnUpdate.getStyleClass().add("toggle-icon4"); +- btnUpdate.setGraphic(SVG.update(Theme.blackFillBinding(), 20, 20)); +- +- updatePane.setHeaderRight(btnUpdate); + } +- +- { +- VBox content = new VBox(); +- content.setSpacing(8); +- +- chkUpdateStable = new JFXRadioButton(i18n("update.channel.stable")); +- chkUpdateDev = new JFXRadioButton(i18n("update.channel.dev")); +- +- TextFlow noteWrapper = new TextFlow(new Text(i18n("update.note"))); +- VBox.setMargin(noteWrapper, new Insets(10, 0, 0, 0)); +- +- content.getChildren().setAll(chkUpdateStable, chkUpdateDev, noteWrapper); +- +- updatePane.getContent().add(content); +- } +- settingsPane.getContent().add(updatePane); + } + + { +@@ -204,11 +150,7 @@ public abstract class SettingsView extends StackPane { + } + } + +- protected abstract void onUpdate(); +- + protected abstract void onExportLogs(); + +- protected abstract void onSponsor(); +- + protected abstract void clearCacheDirectory(); + } +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java +deleted file mode 100644 +index bd60c8ef..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/SponsorPage.java ++++ /dev/null +@@ -1,166 +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.main; +- +-import com.google.gson.annotations.SerializedName; +-import com.google.gson.reflect.TypeToken; +-import com.jfoenix.controls.JFXListCell; +-import com.jfoenix.controls.JFXListView; +-import javafx.geometry.Insets; +-import javafx.geometry.VPos; +-import javafx.scene.Cursor; +-import javafx.scene.control.Label; +-import javafx.scene.layout.*; +-import javafx.scene.text.TextAlignment; +-import org.jackhuang.hmcl.task.Schedulers; +-import org.jackhuang.hmcl.task.Task; +-import org.jackhuang.hmcl.ui.FXUtils; +-import org.jackhuang.hmcl.util.io.HttpRequest; +- +-import java.math.BigDecimal; +-import java.util.Date; +-import java.util.List; +- +-import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +- +-public class SponsorPage extends StackPane { +- private final JFXListView<Sponsor> listView; +- +- public SponsorPage() { +- VBox content = new VBox(); +- content.setPadding(new Insets(10)); +- content.setSpacing(10); +- content.setFillWidth(true); +- +- { +- StackPane sponsorPane = new StackPane(); +- sponsorPane.getStyleClass().add("card"); +- sponsorPane.setCursor(Cursor.HAND); +- sponsorPane.setOnMouseClicked(e -> onSponsor()); +- +- GridPane gridPane = new GridPane(); +- +- ColumnConstraints col = new ColumnConstraints(); +- col.setHgrow(Priority.SOMETIMES); +- col.setMaxWidth(Double.POSITIVE_INFINITY); +- +- gridPane.getColumnConstraints().setAll(col); +- +- RowConstraints row = new RowConstraints(); +- row.setMinHeight(Double.NEGATIVE_INFINITY); +- row.setValignment(VPos.TOP); +- row.setVgrow(Priority.SOMETIMES); +- gridPane.getRowConstraints().setAll(row); +- +- { +- Label label = new Label(i18n("sponsor.hmcl")); +- label.setWrapText(true); +- label.setTextAlignment(TextAlignment.JUSTIFY); +- GridPane.setRowIndex(label, 0); +- GridPane.setColumnIndex(label, 0); +- gridPane.getChildren().add(label); +- } +- +- sponsorPane.getChildren().setAll(gridPane); +- content.getChildren().add(sponsorPane); +- } +- +- { +- StackPane pane = new StackPane(); +- pane.getStyleClass().add("card"); +- listView = new JFXListView<>(); +- listView.setCellFactory((listView) -> new JFXListCell<Sponsor>() { +- @Override +- public void updateItem(Sponsor item, boolean empty) { +- super.updateItem(item, empty); +- if (!empty) { +- setText(item.getName()); +- setGraphic(null); +- } +- } +- }); +- VBox.setVgrow(pane, Priority.ALWAYS); +- pane.getChildren().setAll(listView); +- content.getChildren().add(pane); +- } +- +- loadSponsorList(); +- +- getChildren().setAll(content); +- } +- +- private void onSponsor() { +- FXUtils.openLink("https://hmcl.huangyuhui.net/api/redirect/sponsor"); +- } +- +- private void loadSponsorList() { +- Task.<List<Sponsor>>supplyAsync(() -> HttpRequest.GET("https://hmcl.huangyuhui.net/api/sponsor").getJson(new TypeToken<List<Sponsor>>() { +- }.getType())).thenAcceptAsync(Schedulers.javafx(), sponsors -> { +- listView.getItems().setAll(sponsors); +- }).start(); +- } +- +- private static class Sponsor { +- @SerializedName("name") +- private final String name; +- +- @SerializedName("create_time") +- private final Date createTime; +- +- @SerializedName("money") +- private final BigDecimal money; +- +- @SerializedName("contact") +- private final String contact; +- +- @SerializedName("afdian_id") +- private final String afdianId; +- +- public Sponsor() { +- this("", new Date(), BigDecimal.ZERO, "", ""); +- } +- +- public Sponsor(String name, Date createTime, BigDecimal money, String contact, String afdianId) { +- this.name = name; +- this.createTime = createTime; +- this.money = money; +- this.contact = contact; +- this.afdianId = afdianId; +- } +- +- public String getName() { +- return name; +- } +- +- public Date getCreateTime() { +- return createTime; +- } +- +- public BigDecimal getMoney() { +- return money; +- } +- +- public String getContact() { +- return contact; +- } +- +- public String getAfdianId() { +- return afdianId; +- } +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java +deleted file mode 100644 +index bdb105a3..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java ++++ /dev/null +@@ -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(); +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java +deleted file mode 100644 +index 86d9539b..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java ++++ /dev/null +@@ -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 { +- } +- +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java +deleted file mode 100644 +index 3cc29f8c..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java ++++ /dev/null +@@ -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; +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java +deleted file mode 100644 +index 3110bceb..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java ++++ /dev/null +@@ -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 +- ); +- } +- } +- +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java +deleted file mode 100644 +index 66a0f0ee..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java ++++ /dev/null +@@ -1,123 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import java.io.IOException; +-import java.io.InputStream; +-import java.nio.ByteBuffer; +-import java.nio.channels.FileChannel; +-import java.nio.channels.FileChannel.MapMode; +-import java.nio.file.Path; +-import java.util.Map; +-import java.util.Optional; +-import java.util.zip.ZipEntry; +-import java.util.zip.ZipFile; +- +-import org.jackhuang.hmcl.util.io.IOUtils; +- +-import static java.nio.file.StandardOpenOption.*; +-import static org.jackhuang.hmcl.util.Lang.mapOf; +-import static org.jackhuang.hmcl.util.Pair.pair; +- +-/** +- * Helper class for adding/removing executable header from HMCL file. +- * +- * @author yushijinhun +- */ +-final class ExecutableHeaderHelper { +- private ExecutableHeaderHelper() {} +- +- private static Map<String, String> suffix2header = mapOf( +- pair("exe", "assets/HMCLauncher.exe"), +- pair("sh", "assets/HMCLauncher.sh") +- ); +- +- private static Optional<String> getSuffix(Path file) { +- String filename = file.getFileName().toString(); +- int idxDot = filename.lastIndexOf('.'); +- if (idxDot < 0) { +- return Optional.empty(); +- } else { +- return Optional.of(filename.substring(idxDot + 1)); +- } +- } +- +- private static Optional<byte[]> readHeader(ZipFile zip, String suffix) throws IOException { +- String location = suffix2header.get(suffix); +- if (location != null) { +- ZipEntry entry = zip.getEntry(location); +- if (entry != null && !entry.isDirectory()) { +- try (InputStream in = zip.getInputStream(entry)) { +- return Optional.of(IOUtils.readFullyAsByteArray(in)); +- } +- } +- } +- return Optional.empty(); +- } +- +- private static int detectHeaderLength(ZipFile zip, FileChannel channel) throws IOException { +- ByteBuffer buf = channel.map(MapMode.READ_ONLY, 0, channel.size()); +- suffixLoop: for (String suffix : suffix2header.keySet()) { +- Optional<byte[]> header = readHeader(zip, suffix); +- if (header.isPresent()) { +- buf.rewind(); +- for (byte b : header.get()) { +- if (!buf.hasRemaining() || b != buf.get()) { +- continue suffixLoop; +- } +- } +- return header.get().length; +- } +- } +- return 0; +- } +- +- /** +- * Copies the executable and removes its header. +- */ +- public static void copyWithoutHeader(Path from, Path to) throws IOException { +- try ( +- FileChannel in = FileChannel.open(from, READ); +- FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); +- ZipFile zip = new ZipFile(from.toFile()) +- ) { +- in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); +- } +- } +- +- /** +- * Copies the executable and appends the header according to the suffix. +- */ +- public static void copyWithHeader(Path from, Path to) throws IOException { +- try ( +- FileChannel in = FileChannel.open(from, READ); +- FileChannel out = FileChannel.open(to, CREATE, WRITE, TRUNCATE_EXISTING); +- ZipFile zip = new ZipFile(from.toFile()) +- ) { +- Optional<String> suffix = getSuffix(to); +- if (suffix.isPresent()) { +- Optional<byte[]> header = readHeader(zip, suffix.get()); +- if (header.isPresent()) { +- out.write(ByteBuffer.wrap(header.get())); +- } +- } +- +- in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); +- } +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java +deleted file mode 100644 +index 8b6fdc06..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/HMCLDownloadTask.java ++++ /dev/null +@@ -1,68 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import org.jackhuang.hmcl.task.FileDownloadTask; +-import org.jackhuang.hmcl.util.Pack200Utils; +-import org.jackhuang.hmcl.util.io.NetworkUtils; +-import org.tukaani.xz.XZInputStream; +- +-import java.io.ByteArrayInputStream; +-import java.io.InputStream; +-import java.nio.file.Files; +-import java.nio.file.Path; +-import java.util.jar.JarOutputStream; +- +-class HMCLDownloadTask extends FileDownloadTask { +- +- private RemoteVersion.Type archiveFormat; +- +- public HMCLDownloadTask(RemoteVersion version, Path target) { +- super(NetworkUtils.toURL(version.getUrl()), target.toFile(), version.getIntegrityCheck()); +- archiveFormat = version.getType(); +- } +- +- @Override +- public void execute() throws Exception { +- super.execute(); +- +- try { +- Path target = getFile().toPath(); +- +- switch (archiveFormat) { +- case JAR: +- break; +- +- case PACK_XZ: +- byte[] raw = Files.readAllBytes(target); +- try (InputStream in = new XZInputStream(new ByteArrayInputStream(raw)); +- JarOutputStream out = new JarOutputStream(Files.newOutputStream(target))) { +- Pack200Utils.unpack(in, out); +- } +- break; +- +- default: +- throw new IllegalArgumentException("Unknown format: " + archiveFormat); +- } +- } catch (Throwable e) { +- getFile().delete(); +- throw e; +- } +- } +- +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java +deleted file mode 100644 +index 0bb03df9..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/IntegrityChecker.java ++++ /dev/null +@@ -1,134 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import org.jackhuang.hmcl.util.DigestUtils; +-import org.jackhuang.hmcl.util.io.IOUtils; +-import org.jackhuang.hmcl.util.io.JarUtils; +- +-import java.io.IOException; +-import java.io.InputStream; +-import java.nio.file.Path; +-import java.security.GeneralSecurityException; +-import java.security.KeyFactory; +-import java.security.PublicKey; +-import java.security.Signature; +-import java.security.spec.X509EncodedKeySpec; +-import java.util.Map; +-import java.util.Map.Entry; +-import java.util.TreeMap; +-import java.util.logging.Level; +-import java.util.zip.ZipEntry; +-import java.util.zip.ZipFile; +- +-import static java.nio.charset.StandardCharsets.UTF_8; +-import static org.jackhuang.hmcl.util.Logging.LOG; +- +-/** +- * A class that checks the integrity of HMCL. +- * +- * @author yushijinhun +- */ +-public final class IntegrityChecker { +- private IntegrityChecker() {} +- +- private static final String SIGNATURE_FILE = "META-INF/hmcl_signature"; +- private static final String PUBLIC_KEY_FILE = "assets/hmcl_signature_publickey.der"; +- +- private static PublicKey getPublicKey() throws IOException { +- try (InputStream in = IntegrityChecker.class.getResourceAsStream("/" + PUBLIC_KEY_FILE)) { +- if (in == null) { +- throw new IOException("Public key not found"); +- } +- return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(IOUtils.readFullyAsByteArray(in))); +- } catch (GeneralSecurityException e) { +- throw new IOException("Failed to load public key", e); +- } +- } +- +- private static boolean verifyJar(Path jarPath) throws IOException { +- PublicKey publickey = getPublicKey(); +- +- byte[] signature = null; +- Map<String, byte[]> fileFingerprints = new TreeMap<>(); +- try (ZipFile zip = new ZipFile(jarPath.toFile())) { +- for (ZipEntry entry : zip.stream().toArray(ZipEntry[]::new)) { +- String filename = entry.getName(); +- try (InputStream in = zip.getInputStream(entry)) { +- if (in == null) { +- throw new IOException("entry is null"); +- } +- +- if (SIGNATURE_FILE.equals(filename)) { +- signature = IOUtils.readFullyAsByteArray(in); +- } else { +- fileFingerprints.put(filename, DigestUtils.digest("SHA-512", in)); +- } +- } +- } +- } +- +- if (signature == null) { +- throw new IOException("Signature is missing"); +- } +- +- try { +- Signature verifier = Signature.getInstance("SHA512withRSA"); +- verifier.initVerify(publickey); +- for (Entry<String, byte[]> entry : fileFingerprints.entrySet()) { +- verifier.update(DigestUtils.digest("SHA-512", entry.getKey().getBytes(UTF_8))); +- verifier.update(entry.getValue()); +- } +- return verifier.verify(signature); +- } catch (GeneralSecurityException e) { +- throw new IOException("Failed to verify signature", e); +- } +- } +- +- static void requireVerifiedJar(Path jar) throws IOException { +- if (!verifyJar(jar)) { +- throw new IOException("Invalid signature: " + jar); +- } +- } +- +- private static Boolean selfVerified = null; +- +- /** +- * Checks whether the current application is verified. +- * This method is blocking. +- */ +- public static synchronized boolean isSelfVerified() { +- if (selfVerified != null) { +- return selfVerified; +- } +- try { +- verifySelf(); +- LOG.info("Successfully verified current JAR"); +- selfVerified = true; +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to verify myself, is the JAR corrupt?", e); +- selfVerified = false; +- } +- return selfVerified; +- } +- +- private static void verifySelf() throws IOException { +- Path self = JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location")); +- requireVerifiedJar(self); +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java +deleted file mode 100644 +index c3ac2caa..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/RemoteVersion.java ++++ /dev/null +@@ -1,96 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import com.google.gson.JsonElement; +-import com.google.gson.JsonObject; +-import com.google.gson.JsonParseException; +-import org.jackhuang.hmcl.task.FileDownloadTask.IntegrityCheck; +-import org.jackhuang.hmcl.util.Pack200Utils; +-import org.jackhuang.hmcl.util.gson.JsonUtils; +-import org.jackhuang.hmcl.util.io.NetworkUtils; +- +-import java.io.IOException; +-import java.util.Optional; +- +-public class RemoteVersion { +- +- public static RemoteVersion fetch(UpdateChannel channel, String url) throws IOException { +- try { +- JsonObject response = JsonUtils.fromNonNullJson(NetworkUtils.doGet(NetworkUtils.toURL(url)), JsonObject.class); +- String version = Optional.ofNullable(response.get("version")).map(JsonElement::getAsString).orElseThrow(() -> new IOException("version is missing")); +- String jarUrl = Optional.ofNullable(response.get("jar")).map(JsonElement::getAsString).orElse(null); +- String jarHash = Optional.ofNullable(response.get("jarsha1")).map(JsonElement::getAsString).orElse(null); +- String packXZUrl = Optional.ofNullable(response.get("packxz")).map(JsonElement::getAsString).orElse(null); +- String packXZHash = Optional.ofNullable(response.get("packxzsha1")).map(JsonElement::getAsString).orElse(null); +- if (Pack200Utils.isSupported() && packXZUrl != null && packXZHash != null) { +- return new RemoteVersion(channel, version, packXZUrl, Type.PACK_XZ, new IntegrityCheck("SHA-1", packXZHash)); +- } else if (jarUrl != null && jarHash != null) { +- return new RemoteVersion(channel, version, jarUrl, Type.JAR, new IntegrityCheck("SHA-1", jarHash)); +- } else { +- throw new IOException("No download url is available"); +- } +- } catch (JsonParseException e) { +- throw new IOException("Malformed response", e); +- } +- } +- +- private final UpdateChannel channel; +- private final String version; +- private final String url; +- private final Type type; +- private final IntegrityCheck integrityCheck; +- +- public RemoteVersion(UpdateChannel channel, String version, String url, Type type, IntegrityCheck integrityCheck) { +- this.channel = channel; +- this.version = version; +- this.url = url; +- this.type = type; +- this.integrityCheck = integrityCheck; +- } +- +- public UpdateChannel getChannel() { +- return channel; +- } +- +- public String getVersion() { +- return version; +- } +- +- public String getUrl() { +- return url; +- } +- +- public Type getType() { +- return type; +- } +- +- public IntegrityCheck getIntegrityCheck() { +- return integrityCheck; +- } +- +- @Override +- public String toString() { +- return "[" + version + " from " + url + "]"; +- } +- +- public enum Type { +- PACK_XZ, +- JAR +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java +deleted file mode 100644 +index 998a3da7..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChannel.java ++++ /dev/null +@@ -1,42 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import org.jackhuang.hmcl.Metadata; +- +-public enum UpdateChannel { +- STABLE("stable"), +- DEVELOPMENT("dev"), +- NIGHTLY("nightly"); +- +- public final String channelName; +- +- UpdateChannel(String channelName) { +- this.channelName = channelName; +- } +- +- public static UpdateChannel getChannel() { +- if (Metadata.isDev()) { +- return DEVELOPMENT; +- } else if (Metadata.isNightly()) { +- return NIGHTLY; +- } else { +- return STABLE; +- } +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java +deleted file mode 100644 +index 21ed94d2..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateChecker.java ++++ /dev/null +@@ -1,125 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import javafx.application.Platform; +-import javafx.beans.binding.Bindings; +-import javafx.beans.binding.BooleanBinding; +-import javafx.beans.property.*; +-import javafx.beans.value.ObservableBooleanValue; +-import org.jackhuang.hmcl.Metadata; +-import org.jackhuang.hmcl.util.io.NetworkUtils; +- +-import java.io.IOException; +-import java.util.logging.Level; +- +-import static org.jackhuang.hmcl.util.Lang.mapOf; +-import static org.jackhuang.hmcl.util.Lang.thread; +-import static org.jackhuang.hmcl.util.Logging.LOG; +-import static org.jackhuang.hmcl.util.Pair.pair; +-import static org.jackhuang.hmcl.util.versioning.VersionNumber.asVersion; +- +-public final class UpdateChecker { +- private UpdateChecker() {} +- +- private static ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(); +- private static BooleanBinding outdated = Bindings.createBooleanBinding( +- () -> { +- RemoteVersion latest = latestVersion.get(); +- if (latest == null || isDevelopmentVersion(Metadata.VERSION)) { +- return false; +- } else { +- // We can update from development version to stable version, +- // which can be downgrading. +- return asVersion(latest.getVersion()).compareTo(asVersion(Metadata.VERSION)) != 0; +- } +- }, +- latestVersion); +- private static ReadOnlyBooleanWrapper checkingUpdate = new ReadOnlyBooleanWrapper(false); +- +- public static void init() { +- requestCheckUpdate(UpdateChannel.getChannel()); +- } +- +- public static RemoteVersion getLatestVersion() { +- return latestVersion.get(); +- } +- +- public static ReadOnlyObjectProperty<RemoteVersion> latestVersionProperty() { +- return latestVersion; +- } +- +- public static boolean isOutdated() { +- return outdated.get(); +- } +- +- public static ObservableBooleanValue outdatedProperty() { +- return outdated; +- } +- +- public static boolean isCheckingUpdate() { +- return checkingUpdate.get(); +- } +- +- public static ReadOnlyBooleanProperty checkingUpdateProperty() { +- return checkingUpdate.getReadOnlyProperty(); +- } +- +- private static RemoteVersion checkUpdate(UpdateChannel channel) throws IOException { +- if (!IntegrityChecker.isSelfVerified() && !"true".equals(System.getProperty("hmcl.self_integrity_check.disable"))) { +- throw new IOException("Self verification failed"); +- } +- +- String url = NetworkUtils.withQuery(Metadata.UPDATE_URL, mapOf( +- pair("version", Metadata.VERSION), +- pair("channel", channel.channelName))); +- +- return RemoteVersion.fetch(channel, url); +- } +- +- private static boolean isDevelopmentVersion(String version) { +- return version.contains("@") || // eg. @develop@ +- version.contains("SNAPSHOT"); // eg. 3.1.SNAPSHOT +- } +- +- public static void requestCheckUpdate(UpdateChannel channel) { +- Platform.runLater(() -> { +- if (isCheckingUpdate()) +- return; +- checkingUpdate.set(true); +- +- thread(() -> { +- RemoteVersion result = null; +- try { +- result = checkUpdate(channel); +- LOG.info("Latest version (" + channel + ") is " + result); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to check for update", e); +- } +- +- RemoteVersion finalResult = result; +- Platform.runLater(() -> { +- checkingUpdate.set(false); +- if (finalResult != null) { +- latestVersion.set(finalResult); +- } +- }); +- }, "Update Checker", true); +- }); +- } +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java +deleted file mode 100644 +index 4fa0e2ca..00000000 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/UpdateHandler.java ++++ /dev/null +@@ -1,257 +0,0 @@ +-/* +- * Hello Minecraft! Launcher +- * Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors +- * +- * This program is free software: you can redistribute it and/or modify +- * it under the terms of the GNU General Public License as published by +- * the Free Software Foundation, either version 3 of the License, or +- * (at your option) any later version. +- * +- * This program is distributed in the hope that it will be useful, +- * but WITHOUT ANY WARRANTY; without even the implied warranty of +- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +- * GNU General Public License for more details. +- * +- * You should have received a copy of the GNU General Public License +- * along with this program. If not, see <https://www.gnu.org/licenses/>. +- */ +-package org.jackhuang.hmcl.upgrade; +- +-import com.google.gson.Gson; +-import com.google.gson.JsonParseException; +-import javafx.application.Platform; +- +-import org.jackhuang.hmcl.Main; +-import org.jackhuang.hmcl.Metadata; +-import org.jackhuang.hmcl.task.Task; +-import org.jackhuang.hmcl.task.TaskExecutor; +-import org.jackhuang.hmcl.ui.Controllers; +-import org.jackhuang.hmcl.ui.UpgradeDialog; +-import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; +-import org.jackhuang.hmcl.util.StringUtils; +-import org.jackhuang.hmcl.util.TaskCancellationAction; +-import org.jackhuang.hmcl.util.io.FileUtils; +-import org.jackhuang.hmcl.util.io.JarUtils; +-import org.jackhuang.hmcl.util.platform.JavaVersion; +- +-import javax.swing.*; +-import java.io.IOException; +-import java.nio.file.Files; +-import java.nio.file.Path; +-import java.nio.file.Paths; +-import java.util.*; +-import java.util.concurrent.CancellationException; +-import java.util.logging.Level; +-import java.util.regex.Matcher; +-import java.util.regex.Pattern; +- +-import static org.jackhuang.hmcl.ui.FXUtils.checkFxUserThread; +-import static org.jackhuang.hmcl.util.Lang.thread; +-import static org.jackhuang.hmcl.util.Logging.LOG; +-import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +- +-public final class UpdateHandler { +- private UpdateHandler() {} +- +- /** +- * @return whether to exit +- */ +- public static boolean processArguments(String[] args) { +- breakForceUpdateFeature(); +- +- if (isNestedApplication()) { +- // updated from old versions +- try { +- performMigration(); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to perform migration", e); +- JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE); +- } +- return true; +- } +- +- if (args.length == 2 && args[0].equals("--apply-to")) { +- try { +- applyUpdate(Paths.get(args[1])); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to apply update", e); +- JOptionPane.showMessageDialog(null, i18n("fatal.apply_update_failure", Metadata.PUBLISH_URL) + "\n" + StringUtils.getStackTrace(e), "Error", JOptionPane.ERROR_MESSAGE); +- } +- return true; +- } +- +- if (isFirstLaunchAfterUpgrade()) { +- JOptionPane.showMessageDialog(null, i18n("fatal.migration_requires_manual_reboot"), "Info", JOptionPane.INFORMATION_MESSAGE); +- return true; +- } +- +- return false; +- } +- +- public static void updateFrom(RemoteVersion version) { +- checkFxUserThread(); +- +- Controllers.dialog(new UpgradeDialog(version, () -> { +- Path downloaded; +- try { +- downloaded = Files.createTempFile("hmcl-update-", ".jar"); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to create temp file", e); +- return; +- } +- +- Task<?> task = new HMCLDownloadTask(version, downloaded); +- +- TaskExecutor executor = task.executor(false); +- Controllers.taskDialog(executor, i18n("message.downloading"), TaskCancellationAction.NORMAL); +- executor.start(); +- thread(() -> { +- boolean success = executor.test(); +- +- if (success) { +- try { +- if (!IntegrityChecker.isSelfVerified()) { +- throw new IOException("Current JAR is not verified"); +- } +- +- requestUpdate(downloaded, getCurrentLocation()); +- System.exit(0); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to update to " + version, e); +- Platform.runLater(() -> Controllers.dialog(StringUtils.getStackTrace(e), i18n("update.failed"), MessageType.ERROR)); +- } +- +- } else { +- Exception e = executor.getException(); +- LOG.log(Level.WARNING, "Failed to update to " + version, e); +- if (e instanceof CancellationException) { +- Platform.runLater(() -> Controllers.showToast(i18n("message.cancelled"))); +- } else { +- Platform.runLater(() -> Controllers.dialog(e.toString(), i18n("update.failed"), MessageType.ERROR)); +- } +- } +- }); +- })); +- } +- +- private static void applyUpdate(Path target) throws IOException { +- LOG.info("Applying update to " + target); +- +- Path self = getCurrentLocation(); +- IntegrityChecker.requireVerifiedJar(self); +- ExecutableHeaderHelper.copyWithHeader(self, target); +- +- Optional<Path> newFilename = tryRename(target, Metadata.VERSION); +- if (newFilename.isPresent()) { +- LOG.info("Move " + target + " to " + newFilename.get()); +- try { +- Files.move(target, newFilename.get()); +- target = newFilename.get(); +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to move target", e); +- } +- } +- +- startJava(target); +- } +- +- private static void requestUpdate(Path updateTo, Path self) throws IOException { +- IntegrityChecker.requireVerifiedJar(updateTo); +- startJava(updateTo, "--apply-to", self.toString()); +- } +- +- private static void startJava(Path jar, String... appArgs) throws IOException { +- List<String> commandline = new ArrayList<>(); +- commandline.add(JavaVersion.fromCurrentEnvironment().getBinary().toString()); +- commandline.add("-jar"); +- commandline.add(jar.toAbsolutePath().toString()); +- commandline.addAll(Arrays.asList(appArgs)); +- LOG.info("Starting process: " + commandline); +- new ProcessBuilder(commandline) +- .directory(Paths.get("").toAbsolutePath().toFile()) +- .inheritIO() +- .start(); +- } +- +- private static Optional<Path> tryRename(Path path, String newVersion) { +- String filename = path.getFileName().toString(); +- Matcher matcher = Pattern.compile("^(?<prefix>[hH][mM][cC][lL][.-])(?<version>\\d+(?:\\.\\d+)*)(?<suffix>\\.[^.]+)$").matcher(filename); +- if (matcher.find()) { +- String newFilename = matcher.group("prefix") + newVersion + matcher.group("suffix"); +- if (!newFilename.equals(filename)) { +- return Optional.of(path.resolveSibling(newFilename)); +- } +- } +- return Optional.empty(); +- } +- +- private static Path getCurrentLocation() throws IOException { +- return JarUtils.thisJar().orElseThrow(() -> new IOException("Failed to find current HMCL location")); +- } +- +- // ==== support for old versions === +- private static void performMigration() throws IOException { +- LOG.info("Migrating from old versions"); +- +- Path location = getParentApplicationLocation() +- .orElseThrow(() -> new IOException("Failed to get parent application location")); +- +- requestUpdate(getCurrentLocation(), location); +- } +- +- /** +- * This method must be called from the main thread. +- */ +- private static boolean isNestedApplication() { +- StackTraceElement[] stacktrace = Thread.currentThread().getStackTrace(); +- for (int i = 0; i < stacktrace.length; i++) { +- StackTraceElement element = stacktrace[i]; +- if (Main.class.getName().equals(element.getClassName()) && "main".equals(element.getMethodName())) { +- // we've reached the main method +- return i + 1 != stacktrace.length; +- } +- } +- return false; +- } +- +- private static Optional<Path> getParentApplicationLocation() { +- String command = System.getProperty("sun.java.command"); +- if (command != null) { +- Path path = Paths.get(command); +- if (Files.isRegularFile(path)) { +- return Optional.of(path.toAbsolutePath()); +- } +- } +- return Optional.empty(); +- } +- +- private static boolean isFirstLaunchAfterUpgrade() { +- Optional<Path> currentPath = JarUtils.thisJar(); +- if (currentPath.isPresent()) { +- Path updated = Metadata.HMCL_DIRECTORY.resolve("HMCL-" + Metadata.VERSION + ".jar"); +- if (currentPath.get().toAbsolutePath().equals(updated.toAbsolutePath())) { +- return true; +- } +- } +- return false; +- } +- +- private static void breakForceUpdateFeature() { +- Path hmclVersionJson = Metadata.HMCL_DIRECTORY.resolve("hmclver.json"); +- if (Files.isRegularFile(hmclVersionJson)) { +- try { +- Map<?, ?> content = new Gson().fromJson(FileUtils.readText(hmclVersionJson), Map.class); +- Object ver = content.get("ver"); +- if (ver instanceof String && ((String) ver).startsWith("3.")) { +- Files.delete(hmclVersionJson); +- LOG.info("Successfully broke the force update feature"); +- } +- } catch (IOException e) { +- LOG.log(Level.WARNING, "Failed to break the force update feature", e); +- } catch (JsonParseException e) { +- hmclVersionJson.toFile().delete(); +- } +- } +- } +- // ==== +-} +diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +index 252265be..53b93d43 100644 +--- a/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java ++++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/CrashReporter.java +@@ -23,8 +23,6 @@ import javafx.scene.control.Alert.AlertType; + import org.jackhuang.hmcl.Metadata; + import org.jackhuang.hmcl.countly.CrashReport; + import org.jackhuang.hmcl.ui.CrashWindow; +-import org.jackhuang.hmcl.upgrade.IntegrityChecker; +-import org.jackhuang.hmcl.upgrade.UpdateChecker; + import org.jackhuang.hmcl.util.io.NetworkUtils; + + import java.io.IOException; +@@ -106,9 +104,6 @@ public class CrashReporter implements Thread.UncaughtExceptionHandler { + if (showCrashWindow) { + new CrashWindow(text).show(); + } +- if (!UpdateChecker.isOutdated() && IntegrityChecker.isSelfVerified()) { +- reportToServer(report); +- } + } + }); + } catch (Throwable handlingException) { +-- +2.38.1 + @@ -1,35 +1,65 @@ +# shellcheck shell=bash # Maintainer: AvianaCruz <gwencroft <at> proton <dot> me> # Contributor: Rowisi < nomail <at> private <dot> com > # Contributor: So1ar <so1ar114514 <at> gmail <dot> com> pkgname=hmcl-new +_pkgname=HMCL _ver=3.5.3 -_build=223 +_build=227 pkgver=$_ver.$_build pkgrel=1 pkgdesc='An unofficial build of HMCL that trying to compile and run HMCL with the latest LTS version of java.' arch=('any') -url='https://github.com/skbeh/HMCL-build' +url='https://github.com/huanghongxun/HMCL' license=('GPL3') depends=('java-openjfx>=17') +makedepends=('java-environment>=17' 'gradle') provides=('hmcl') conflicts=('hmcl') source=('hmcl.desktop' 'hmcl-launch-script' 'craft_table.png' - 'LICENSE::https://raw.githubusercontent.com/huanghongxun/HMCL/javafx/LICENSE' - "$pkgname-$pkgver-$pkgrel.jar::$url/releases/download/v$pkgver/HMCL-$pkgver.jar") + "${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz" + "0001-Target-java-17.patch" + "0002-Cleanup.patch" +) sha256sums=('b4e8aa0f349bb3f5dd15a31c5a13ac3e10e5a5bcd2f97cf390041924275e43ef' - '534e391a637394e47cdeb0d9dfe24cd6fd1dedb863c085951403ec24f1470d06' + '9adb4243a5123ff82cb3678ebb3e889250d745973859d57ab5a14b2867b7cb04' '2989a1b5301b8c7b9afdae5696c6a4e5246afa2d4f1f3d3dad5c192f036a9b4c' - '3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986' - 'd3680eb1f126f1d9cc2239bf68387165ce2d91b3cc9cd510a8339dddb68fb94d') -noextract=("$pkgname-$pkgver-$pkgrel.jar") + '6d0b1fa5d4a7cab1024e62c12ea6baf19197175e2d2d3af9b23099878057b92f' + '6348216b7c7c9b4d44355d19e11ea6d27d7b1d48d3f0a43079ab929e70728448' + '34b2d477abed1858dc36069fc4374510f9ef52632fd415e0a077f99240ee20e6') + +prepare() { + cd "$_pkgname-$pkgver" + local src + for src in "${source[@]}"; do + src="${src%%::*}" + src="${src##*/}" + [[ $src = *.patch ]] || continue + patch -Np1 <"../$src" + done +} + +build() { + cd "$_pkgname-$pkgver" + VERSION_TYPE=stable \ + VERSION_ROOT="$_ver" BUILD_NUMBER="$_build" \ + MICROSOFT_AUTH_ID='6a3728d6-27a3-4180-99bb-479895b8f88e' MICROSOFT_AUTH_SECRET='dR.50SWwVez4-PQOF2-e_2GHmC~4Xl-p4p' \ + CURSEFORGE_API_KEY='$2a$10$o8pygPrhvKBHuuh5imL2W.LCNFhB15zBYAExXx/TqTx/Zp5px2lxu' \ + gradle build -x test +} + +check() { + cd "$_pkgname-$pkgver" + gradle test +} package() { install -Dm755 'hmcl-launch-script' "$pkgdir/usr/bin/$pkgname" install -Dm644 'hmcl.desktop' "$pkgdir/usr/share/applications/$pkgname.desktop" - install -Dm644 "$pkgname-$pkgver-$pkgrel.jar" "$pkgdir/usr/share/java/$pkgname/$pkgname.jar" + install -Dm644 "$_pkgname-$pkgver/HMCL/build/libs/$_pkgname-$pkgver.jar" "$pkgdir/usr/share/java/$pkgname/$pkgname.jar" install -Dm644 'craft_table.png' "$pkgdir/usr/share/icons/hicolor/48x48/apps/$pkgname.png" - install -Dm644 'LICENSE' "$pkgdir/usr/share/licenses/$pkgname/LICENSE" + install -Dm644 "$_pkgname-$pkgver/LICENSE" "$pkgdir/usr/share/licenses/$pkgname/LICENSE" } diff --git a/hmcl-launch-script b/hmcl-launch-script index 029992d599ac..35ad0c0f05f3 100755 --- a/hmcl-launch-script +++ b/hmcl-launch-script @@ -1,11 +1,12 @@ #!/bin/sh +set -eu -if [[ "$1" != "-p" ]]; then +if [ "${1:-}" != -p ]; then WORKDIR=${XDG_CONFIG_HOME:-$HOME/.config}/hmcl - mkdir -p $WORKDIR - cd $WORKDIR + mkdir -p "$WORKDIR" + cd "$WORKDIR" fi -JAVA_HOME=/usr/lib/jvm/$(pacman -Ql java-openjfx | grep javafx | cut -d "/" -f 5 | head -n 1) +JAVA_HOME=/usr/lib/jvm/$(pacman -Ql java-openjfx | grep javafx | head -n 1 | cut -d / -f 5) -exec ${JAVA_HOME}/bin/java --module-path ${JAVA_HOME}/lib/javafx.base.jar:${JAVA_HOME}/lib/javafx.fxml.jar:${JAVA_HOME}/lib/javafx.graphics.jar:${JAVA_HOME}/lib/javafx.media.jar:${JAVA_HOME}/lib/javafx.swing.jar:${JAVA_HOME}/lib/javafx.web.jar:${JAVA_HOME}/lib/javafx.controls.jar --add-modules=javafx.base --add-modules=javafx.fxml --add-modules=javafx.graphics --add-modules=javafx.media --add-modules=javafx.swing --add-modules=javafx.web --add-modules=javafx.controls -jar "/usr/share/java/hmcl-new/hmcl-new.jar" "$@" +exec "${JAVA_HOME}"/bin/java --module-path "${JAVA_HOME}/lib/javafx.base.jar:${JAVA_HOME}/lib/javafx.fxml.jar:${JAVA_HOME}/lib/javafx.graphics.jar:${JAVA_HOME}/lib/javafx.media.jar:${JAVA_HOME}/lib/javafx.swing.jar:${JAVA_HOME}/lib/javafx.web.jar:${JAVA_HOME}/lib/javafx.controls.jar" --add-modules=javafx.base --add-modules=javafx.fxml --add-modules=javafx.graphics --add-modules=javafx.media --add-modules=javafx.swing --add-modules=javafx.web --add-modules=javafx.controls -jar /usr/share/java/hmcl-new/hmcl-new.jar "$@" |