diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index fc7cf0254..1ae40f22b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -99,6 +99,10 @@ public final class LauncherHelper { showLogs = true; } + public void setKeep() { + launcherVisibility = LauncherVisibility.KEEP; + } + public void launch() { Logging.LOG.info("Launching game version: " + selectedVersion); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogPane.java index 2dac53f36..c98f902e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/DialogPane.java @@ -35,8 +35,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DialogPane extends JFXDialogLayout { private final StringProperty title = new SimpleStringProperty(); private final BooleanProperty valid = new SimpleBooleanProperty(); - private final SpinnerPane acceptPane = new SpinnerPane(); - private final Label warningLabel = new Label(); + protected final SpinnerPane acceptPane = new SpinnerPane(); + protected final JFXButton cancelButton = new JFXButton(); + protected final Label warningLabel = new Label(); private final JFXProgressBar progressBar = new JFXProgressBar(); public DialogPane() { @@ -57,7 +58,7 @@ public class DialogPane extends JFXDialogLayout { acceptPane.getStyleClass().add("small-spinner-pane"); acceptPane.setContent(acceptButton); - JFXButton cancelButton = new JFXButton(i18n("button.cancel")); + cancelButton.setText(i18n("button.cancel")); cancelButton.setOnAction(e -> onCancel()); cancelButton.getStyleClass().add("dialog-cancel"); onEscPressed(this, cancelButton::fire); 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 15038d2ae..6f23e44e7 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 @@ -228,8 +228,7 @@ public final class MainPage extends StackPane implements DecoratorPage { } private void launch() { - Profile profile = Profiles.getSelectedProfile(); - Versions.launch(profile, profile.getSelectedVersion()); + Versions.launch(Profiles.getSelectedProfile()); } private void onMenu() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java index 7366b8323..c210798e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java @@ -17,6 +17,9 @@ */ package org.jackhuang.hmcl.ui.multiplayer; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.layout.ColumnConstraints; @@ -24,10 +27,7 @@ import javafx.scene.layout.GridPane; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.construct.DialogAware; -import org.jackhuang.hmcl.ui.construct.DialogPane; -import org.jackhuang.hmcl.ui.construct.HintPane; -import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.util.FutureCallback; import java.util.Objects; @@ -38,12 +38,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware { - private final FutureCallback callback; + private final StringProperty token = new SimpleStringProperty(); + private final FutureCallback callback; private final LocalServerDetector lanServerDetectorThread; private LocalServerDetector.PingResponse server; - CreateMultiplayerRoomDialog(FutureCallback callback) { + CreateMultiplayerRoomDialog(FutureCallback callback) { this.callback = callback; setTitle(i18n("multiplayer.session.create")); @@ -62,23 +63,34 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa body.addRow(0, hintPane); + JFXTextField tokenField = new JFXTextField(); + tokenField.textProperty().bindBidirectional(token); + tokenField.setPromptText(i18n("multiplayer.session.create.token.prompt")); + body.addRow(1, new Label(i18n("multiplayer.session.create.token")), tokenField); + Label nameField = new Label(); nameField.setText(Optional.ofNullable(Accounts.getSelectedAccount()) .map(Account::getUsername) .map(username -> i18n("multiplayer.session.name.format", username)) .orElse("")); - body.addRow(1, new Label(i18n("multiplayer.session.create.name")), nameField); + body.addRow(2, new Label(i18n("multiplayer.session.create.name")), nameField); Label portLabel = new Label(i18n("multiplayer.nat.testing")); portLabel.setText(i18n("multiplayer.nat.testing")); - body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel); + body.addRow(3, new Label(i18n("multiplayer.session.create.port")), portLabel); setValid(false); + JFXHyperlink noinLink = new JFXHyperlink(); + noinLink.setText("noin.cn"); + noinLink.setOnAction(e -> FXUtils.openLink("https://noin.cn")); + + setActions(warningLabel, noinLink, acceptPane, cancelButton); + lanServerDetectorThread = new LocalServerDetector(3); lanServerDetectorThread.onDetectedLanServer().register(event -> { runInFX(() -> { - if (event.getLanServer().isValid()) { + if (event.getLanServer() != null && event.getLanServer().isValid()) { nameField.setText(event.getLanServer().getMotd()); portLabel.setText(event.getLanServer().getAd().toString()); setValid(true); @@ -86,6 +98,7 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa nameField.setText(""); portLabel.setText(""); onFailure(i18n("multiplayer.session.create.port.error")); + setValid(false); } server = event.getLanServer(); body.setDisable(false); @@ -98,7 +111,7 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa protected void onAccept() { setLoading(); - callback.call(Objects.requireNonNull(server), () -> { + callback.call(new CreationResult(token.get(), Objects.requireNonNull(server)), () -> { runInFX(this::onSuccess); }, msg -> { runInFX(() -> onFailure(msg)); @@ -116,4 +129,22 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa public void onDialogClosed() { lanServerDetectorThread.interrupt(); } + + public static class CreationResult { + private final String token; + private final LocalServerDetector.PingResponse server; + + public CreationResult(String token, LocalServerDetector.PingResponse server) { + this.token = token; + this.server = server; + } + + public String getToken() { + return token; + } + + public LocalServerDetector.PingResponse getServer() { + return server; + } + } } 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 index 213fc9784..040d66bac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java @@ -25,6 +25,7 @@ import org.jackhuang.hmcl.launch.StreamPump; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.platform.Architecture; @@ -76,7 +77,7 @@ public final class MultiplayerManager { return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH); } - public static CatoSession joinSession(String version, String sessionName, String peer, int remotePort, int localPort) throws IOException, IncompatibleCatoVersionException { + public static CatoSession joinSession(String token, String version, String sessionName, String peer, int remotePort, int localPort) throws IOException, IncompatibleCatoVersionException { if (!CATO_VERSION.equals(version)) { throw new IncompatibleCatoVersionException(version, CATO_VERSION); } @@ -85,7 +86,12 @@ public final class MultiplayerManager { if (!Files.isRegularFile(exe)) { throw new IllegalStateException("Cato file not found"); } - String[] commands = new String[]{exe.toString(), "--token", "new", "--peer", peer, "--local", String.format("127.0.0.1:%d", localPort), "--remote", String.format("127.0.0.1:%d", remotePort)}; + String[] commands = new String[]{exe.toString(), + "--token", StringUtils.isBlank(token) ? "new" : token, + "--peer", peer, + "--local", String.format("127.0.0.1:%d", localPort), + "--remote", String.format("127.0.0.1:%d", remotePort), + "--mode", "relay"}; Process process = new ProcessBuilder() .command(commands) .start(); @@ -95,7 +101,7 @@ public final class MultiplayerManager { return session; } - public static CatoSession createSession(String sessionName, int port) throws IOException { + public static CatoSession createSession(String token, String sessionName, int port) throws IOException { Path exe = getCatoExecutable(); if (!Files.isRegularFile(exe)) { throw new IllegalStateException("Cato file not found"); @@ -104,7 +110,10 @@ public final class MultiplayerManager { // MultiplayerServer server = new MultiplayerServer(port); // server.start(); - String[] commands = new String[]{exe.toString(), "--token", "new", "--allows", String.format("127.0.0.1:%d", port)}; + String[] commands = new String[]{exe.toString(), + "--token", StringUtils.isBlank(token) ? "new" : token, + "--allows", String.format("127.0.0.1:%d", port), + "--mode", "relay"}; Process process = new ProcessBuilder() .command(commands) .start(); 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 index 728099c6a..a9752a7c1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java @@ -169,9 +169,9 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> { - int port = result.getAd(); + int port = result.getServer().getAd(); try { - initCatoSession(MultiplayerManager.createSession(result.getMotd(), port)); + initCatoSession(MultiplayerManager.createSession(result.getToken(), result.getServer().getMotd(), port)); } catch (Exception e) { LOG.log(Level.WARNING, "Failed to create session", e); reject.accept(i18n("multiplayer.session.create.error")); @@ -190,7 +190,8 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> { - String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue(); + String token = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue(); + String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(2)).getValue(); MultiplayerManager.Invitation invitation; try { invitation = MultiplayerManager.parseInvitationCode(invitationCode); @@ -209,7 +210,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } try { - initCatoSession(MultiplayerManager.joinSession(invitation.getVersion(), invitation.getSessionName(), invitation.getId(), invitation.getGamePort(), localPort)); + initCatoSession(MultiplayerManager.joinSession(token, invitation.getVersion(), invitation.getSessionName(), invitation.getId(), invitation.getGamePort(), localPort)); } catch (MultiplayerManager.IncompatibleCatoVersionException e) { reject.accept(i18n("multiplayer.session.join.invitation_code.version")); return; @@ -223,6 +224,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware resolve.run(); }) .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("multiplayer.session.join.hint"))) + .addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.create.token"), "")) .addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator()))); } 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 index c5a85e39c..fbfeee9ba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java @@ -25,11 +25,15 @@ import javafx.scene.control.ScrollPane; import javafx.scene.control.SkinBase; import javafx.scene.layout.*; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.game.LauncherHelper; +import org.jackhuang.hmcl.setting.Profile; +import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.util.javafx.BindingMapping; import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; @@ -53,31 +57,37 @@ public class MultiplayerPageSkin extends SkinBase { AdvancedListItem createRoomItem = new AdvancedListItem(); createRoomItem.setTitle(i18n("multiplayer.session.create")); createRoomItem.setLeftGraphic(wrap(SVG::plusCircleOutline)); + createRoomItem.setActionButtonVisible(false); createRoomItem.setOnAction(e -> control.createRoom()); AdvancedListItem joinRoomItem = new AdvancedListItem(); joinRoomItem.setTitle(i18n("multiplayer.session.join")); joinRoomItem.setLeftGraphic(wrap(SVG::accountArrowRightOutline)); + joinRoomItem.setActionButtonVisible(false); joinRoomItem.setOnAction(e -> control.joinRoom()); AdvancedListItem copyLinkItem = new AdvancedListItem(); copyLinkItem.setTitle(i18n("multiplayer.session.copy_room_code")); copyLinkItem.setLeftGraphic(wrap(SVG::accountArrowRightOutline)); + copyLinkItem.setActionButtonVisible(false); copyLinkItem.setOnAction(e -> control.copyInvitationCode()); AdvancedListItem cancelItem = new AdvancedListItem(); cancelItem.setTitle(i18n("button.cancel")); cancelItem.setLeftGraphic(wrap(SVG::closeCircle)); + cancelItem.setActionButtonVisible(false); cancelItem.setOnAction(e -> control.cancelRoom()); AdvancedListItem quitItem = new AdvancedListItem(); quitItem.setTitle(i18n("multiplayer.session.quit")); quitItem.setLeftGraphic(wrap(SVG::closeCircle)); + quitItem.setActionButtonVisible(false); quitItem.setOnAction(e -> control.quitRoom()); AdvancedListItem closeRoomItem = new AdvancedListItem(); closeRoomItem.setTitle(i18n("multiplayer.session.close")); closeRoomItem.setLeftGraphic(wrap(SVG::closeCircle)); + closeRoomItem.setActionButtonVisible(false); closeRoomItem.setOnAction(e -> control.closeRoom()); FXUtils.onChangeAndOperate(getSkinnable().multiplayerStateProperty(), state -> { @@ -94,6 +104,14 @@ public class MultiplayerPageSkin extends SkinBase { } AdvancedListBox sideBar = new AdvancedListBox() + .addNavigationDrawerItem(item -> { + item.setTitle(i18n("version.launch")); + item.setLeftGraphic(wrap(SVG::rocketLaunchOutline)); + item.setOnAction(e -> { + Profile profile = Profiles.getSelectedProfile(); + Versions.launch(profile, profile.getSelectedVersion(), LauncherHelper::setKeep); + }); + }) .startCategory(i18n("multiplayer.session")) .add(roomPane) .startCategory(i18n("help")) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java index d991d1ad1..2999f1e8f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java @@ -208,22 +208,27 @@ public final class Versions { }); } + public static void launch(Profile profile) { + launch(profile, profile.getSelectedVersion()); + } + public static void launch(Profile profile, String id) { + launch(profile, id, null); + } + + public static void launch(Profile profile, String id, Consumer injecter) { if (!checkVersionForLaunching(profile, id)) return; ensureSelectedAccount(account -> { - new LauncherHelper(profile, account, id).launch(); + LauncherHelper launcherHelper = new LauncherHelper(profile, account, id); + if (injecter != null) + injecter.accept(launcherHelper); + launcherHelper.launch(); }); } public static void testGame(Profile profile, String id) { - if (!checkVersionForLaunching(profile, id)) - return; - ensureSelectedAccount(account -> { - LauncherHelper helper = new LauncherHelper(profile, account, id); - helper.setTestMode(); - helper.launch(); - }); + launch(profile, id, LauncherHelper::setTestMode); } private static boolean checkVersionForLaunching(Profile profile, String id) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 14ebf82ce..1f776fddc 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -620,6 +620,8 @@ multiplayer.session.create.hint=Before creating multiplayer room, you must click multiplayer.session.create.name=Session Name multiplayer.session.create.port=Port multiplayer.session.create.port.error=Cannot detect game port, you must click "Open LAN Server" in game to enable multiplayer functionality. +multiplayer.session.create.token=Token +multiplayer.session.create.token.prompt=Default randomized. You can apply for your own token at noin.cn (Chinese website). multiplayer.session.expired=Multiplayer session has expired. You should re-create or re-join a room to continue. multiplayer.session.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality. multiplayer.session.join=Join Room diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index bc894521e..3d3bb3506 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -620,6 +620,8 @@ multiplayer.session.create.hint=創建聯機房間前,你需要先在正在運 multiplayer.session.create.name=房間名稱 multiplayer.session.create.port=埠號 multiplayer.session.create.port.error=無法檢測遊戲埠號,你必須先啟動遊戲並在遊戲內打開對區域網路開放選項後才能啟動聯機。 +multiplayer.session.create.token=Token +multiplayer.session.create.token.prompt=預設為臨時 Token。你可以在 noin.cn 上申請靜態 Token 並填寫至此處 multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。 multiplayer.session.join=加入房間 multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 3ded5ccc6..6de795655 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -616,10 +616,12 @@ multiplayer.session.close.warning=关闭房间后,已经加入联机房间的 multiplayer.session.copy_room_code=复制邀请码 multiplayer.session.create=创建房间 multiplayer.session.create.error=创建联机房间失败。 -multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中输入游戏内提示的端口号(通常是 5 位的数字) +multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中确认游戏内提示的端口号(通常是 5 位的数字) multiplayer.session.create.name=房间名称 multiplayer.session.create.port=端口号 multiplayer.session.create.port.error=无法检测游戏端口号,你必须先启动游戏并在游戏内打开对局域网开放选项后才能启动联机。 +multiplayer.session.create.token=Token +multiplayer.session.create.token.prompt=默认为临时 Token。你可以在 noin.cn 上申请静态 Token 并填写至此处 multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。 multiplayer.session.join=加入房间 multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java index cfe90ca08..2a3235fa8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/auth/offline/YggdrasilServer.java @@ -62,7 +62,7 @@ public class YggdrasilServer extends HttpServer { "localhost" )), pair("meta", mapOf( - pair("serverName", "HMCL Offline Account Skin/Cape Server"), + pair("serverName", "HMCL"), pair("implementationName", "HMCL"), pair("implementationVersion", "1.0"), pair("feature.non_email_login", true)