From 441ce7220fe80e799afc8588a53bb2a39eb49323 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Sun, 8 Nov 2026 05:29:26 +0800 Subject: [PATCH] feat: one-click join server button using manifest serverAddress Co-Authored-By: Claude Sonnet 5.5 --- .../java/org/jackhuang/hmcl/Metadata.java | 7 +++ .../jackhuang/hmcl/game/ServerEndpoint.java | 47 +++++++++++++++++++ .../hmcl/ui/instances/Instances.java | 18 +++++++ .../org/jackhuang/hmcl/ui/main/MainPage.java | 4 +- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_lzh.properties | 1 + .../assets/lang/I18N_zh_Hans.properties | 1 + .../assets/lang/I18N_zh_Hant.properties | 1 + .../modpack/server/ServerModpackManifest.java | 14 ++++++ 9 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/ServerEndpoint.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java index 78b69a8bd8..d9fb880c53 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/Metadata.java @@ -141,4 +141,11 @@ public final class Metadata { return null; } + /// Server address used when the installed modpack manifest does not provide one. + /// + /// Can be set at build or run time with the `hmcl.server.address` system property; empty by default. + public static String getDefaultServerAddress() { + return System.getProperty("hmcl.server.address", "").trim(); + } + } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ServerEndpoint.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ServerEndpoint.java new file mode 100644 index 0000000000..caa96c9530 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ServerEndpoint.java @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors +// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL +package org.jackhuang.hmcl.game; + +import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.modpack.ModpackConfiguration; +import org.jackhuang.hmcl.modpack.server.ServerModpackManifest; +import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/// Resolves the server that the one-click join button connects to. +@NotNullByDefault +public final class ServerEndpoint { + private ServerEndpoint() { + } + + /// Finds the server address for an instance. + /// + /// The address stored in the installed modpack manifest wins, so it changes with modpack updates. + /// [Metadata#getDefaultServerAddress()] is used as the fallback. + /// + /// @param configurationFile the instance's `modpack.json` path + /// @return the `host[:port]` address, or `null` when none is configured + public static @Nullable String resolve(Path configurationFile) { + if (Files.isRegularFile(configurationFile)) { + try { + ModpackConfiguration configuration = + JsonUtils.fromJsonFile(configurationFile, ModpackConfiguration.typeOf(ServerModpackManifest.class)); + String address = configuration != null ? configuration.getManifest().getServerAddress() : null; + if (StringUtils.isNotBlank(address)) { + return address.trim(); + } + } catch (Exception e) { + LOG.warning("Unable to read server address from " + configurationFile, e); + } + } + String fallback = Metadata.getDefaultServerAddress(); + return fallback.isEmpty() ? null : fallback; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 5c2a137f23..037e4957b2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -329,6 +329,24 @@ public final class Instances { }); } + /// Launches the instance and connects straight to the server configured for it. + /// + /// When no server address is configured the game starts at the title screen. + /// + /// @param gameInstance the instance to launch, or `null` when none is available + public static void joinServer(@Nullable HMCLGameInstance gameInstance) { + if (gameInstance == null) { + launch(null); + return; + } + @Nullable String address = ServerEndpoint.resolve(gameInstance.getModpackConfigurationFile()); + if (address == null) { + launch(gameInstance); + } else { + launch(gameInstance, helper -> helper.setQuickPlayOption(new QuickPlayOption.MultiPlayer(address))); + } + } + public static void testGame(HMCLGameInstance gameInstance) { launch(gameInstance, LauncherHelper::setTestMode); } 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 702850522a..6482721027 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 @@ -119,7 +119,7 @@ public final class MainPage extends StackPane implements DecoratorPage { tooltip = new Tooltip(i18n("instance.launch.empty.tooltip")); FXUtils.installFastTooltip(launchButton, tooltip); } else { - launchLabel.setText(i18n("instance.launch")); + launchLabel.setText(i18n("server.join")); currentLabel.setText(currentGame.getId().toString()); graphic.getChildren().setAll(launchLabel, currentLabel); FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launch); @@ -188,7 +188,7 @@ public final class MainPage extends StackPane implements DecoratorPage { private void launch() { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - Instances.launch(repository.getSelectedInstance()); + Instances.joinServer(repository.getSelectedInstance()); } /// Opens the server modpack install/update flow when no instance is selected. diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 397a047fca..1019541058 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -791,6 +791,7 @@ instance.game.support_status.unsupported=Unsupported instance.game.support_status.untested=Untested instance.game.type=Type instance.launch=Launch Game +server.join=Join Server instance.launch_and_enter_world=Play World instance.launch.empty=Start Game instance.launch.empty.installing=Installing Game diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 6bea50fe42..754c5c2963 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -611,6 +611,7 @@ instance.game.snapshots=預版 # instance.game.support_status.untested= instance.game.type=版類 instance.launch=戲 +server.join=入服 # instance.launch_and_enter_world= instance.launch.empty=啟戲 instance.launch.empty.installing=裝戲 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties index 746fd278dc..739433a178 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -614,6 +614,7 @@ instance.game.support_status.unsupported=不支持 instance.game.support_status.untested=未经测试 instance.game.type=版本类型 instance.launch=启动游戏 +server.join=进入服务器 instance.launch_and_enter_world=进入世界 instance.launch.empty=开始游戏 instance.launch.empty.installing=安装游戏 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties index 50930c7fd3..e2cb2bf8c8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -608,6 +608,7 @@ instance.game.support_status.unsupported=不支援 instance.game.support_status.untested=未經測試 instance.game.type=版本類型 instance.launch=啟動遊戲 +server.join=進入伺服器 instance.launch_and_enter_world=進入世界 instance.launch.empty=開始遊戲 instance.launch.empty.installing=安裝遊戲 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java index 3fb9629be5..1a2010473a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java @@ -29,12 +29,19 @@ public class ServerModpackManifest implements ModpackManifest, Validation { private final String fileApi; private final List files; private final List addons; + /// The address players are connected to on launch, or `null` when the manifest does not specify one. + private final @Nullable String serverAddress; public ServerModpackManifest() { this("", "", "", "", "", Collections.emptyList(), Collections.emptyList()); } public ServerModpackManifest(String name, String author, String version, String description, String fileApi, List files, List addons) { + this(name, author, version, description, fileApi, files, addons, null); + } + + public ServerModpackManifest(String name, String author, String version, String description, String fileApi, List files, List addons, @Nullable String serverAddress) { + this.serverAddress = serverAddress; this.name = name; this.author = author; this.version = version; @@ -60,6 +67,13 @@ public class ServerModpackManifest implements ModpackManifest, Validation { return description; } + /// Returns the server address to connect to after launch. + /// + /// @return the `host[:port]` address, or `null` when the manifest does not specify one + public @Nullable String getServerAddress() { + return serverAddress; + } + public String getFileApi() { return fileApi; }