From 4ee5c0984f81f2c7ec5a8c5d0dd5c0d64a4e5525 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Wed, 11 Nov 2026 12:49:09 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E9=AB=98=E6=80=A7=E8=83=BD=20GPU?= =?UTF-8?q?=20=E9=BB=98=E8=AE=A4=E5=BC=80=E5=90=AF=E3=80=81=E9=A6=96?= =?UTF-8?q?=E9=A1=B5=E6=98=BE=E7=A4=BA=E6=9C=8D=E5=8A=A1=E5=99=A8=20MOTD?= =?UTF-8?q?=20=E5=85=AC=E5=91=8A=E3=80=81=E6=B8=B8=E6=88=8F=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E9=87=8D=E5=90=AF=E5=90=8E=E6=9B=B4=E6=96=B0=E5=B9=B6?= =?UTF-8?q?=E9=87=8D=E5=90=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - highPerformance 默认 true(Windows 同时登记 java.exe 与 javaw.exe),Linux AMD/nouveau 加 DRI_PRIME - ServerStatusPing 解析 description,首页在服务器公告区显示,失败静默 - 游戏正常退出且运行目录有 tdlm-restart-request.json 时,删除文件并询问是否更新并重启 Co-Authored-By: Claude Sonnet 5.5 --- .../jackhuang/hmcl/game/LauncherHelper.java | 65 +++++++++++-------- .../jackhuang/hmcl/game/RestartRequest.java | 56 ++++++++++++++++ .../jackhuang/hmcl/setting/GameSettings.java | 5 +- .../hmcl/ui/instances/Instances.java | 15 +++++ .../org/jackhuang/hmcl/ui/main/MainPage.java | 37 +++++++++-- .../resources/assets/lang/I18N.properties | 4 ++ .../resources/assets/lang/I18N_lzh.properties | 4 ++ .../assets/lang/I18N_zh_Hans.properties | 4 ++ .../assets/lang/I18N_zh_Hant.properties | 4 ++ .../hmcl/game/RestartRequestTest.java | 36 ++++++++++ .../hmcl/launch/DefaultLauncher.java | 4 ++ .../jackhuang/hmcl/util/ServerStatusPing.java | 45 +++++++++++-- .../hmcl/util/ServerStatusPingTest.java | 10 ++- 13 files changed, 251 insertions(+), 38 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/game/RestartRequest.java create mode 100644 HMCL/src/test/java/org/jackhuang/hmcl/game/RestartRequestTest.java 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 7a5d9662d7..4185da0382 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -255,32 +255,12 @@ public final class LauncherHelper { } if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) { - @Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable); - try { - WinReg reg = WinReg.INSTANCE; - if (reg != null) { - Object current = reg.queryValue( - WinReg.HKEY.HKEY_CURRENT_USER, - "Software\\Microsoft\\DirectX\\UserGpuPreferences", - javaPathGpuReg - ); - if (current == null) { - boolean result = reg.setValue( - WinReg.HKEY.HKEY_CURRENT_USER, - "Software\\Microsoft\\DirectX\\UserGpuPreferences", - javaPathGpuReg, - "GpuPreference=2;" - ); - if (result) - LOG.info("Successfully applied high performance GPU preference for java: " + javaPathGpuReg); - else - LOG.warning("Failed to apply high performance GPU preference for java: " + javaPathGpuReg); - } else { - LOG.info("GPU preference for %s already exists: %s".formatted(javaPathGpuReg, current)); - } - } - } catch (Exception e) { - LOG.warning("Failed to apply high performance GPU preference", e); + // Register both java.exe and javaw.exe: the game may be started through either one. + applyHighPerformanceGpuPreference(context.javaExecutable); + Path sibling = context.javaExecutable.resolveSibling( + FileUtils.getName(context.javaExecutable).equalsIgnoreCase("javaw.exe") ? "java.exe" : "javaw.exe"); + if (Files.isRegularFile(sibling)) { + applyHighPerformanceGpuPreference(sibling); } } @@ -1048,11 +1028,44 @@ public final class LauncherHelper { runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show()); } + if (exitType == ExitType.NORMAL) { + // The game asked to be restarted (hot update): offer to update and relaunch. + String restartReason = RestartRequest.consume(gameInstance.getRunDirectory()); + if (restartReason != null) { + LOG.info("Game requested a restart, reason: " + restartReason); + runLater(() -> Instances.offerRestart(gameInstance, restartReason)); + } + } + checkExit(); } } + /// Asks Windows to run `executable` on the high-performance GPU (`GpuPreference=2`) unless the user + /// already has a preference stored for it. + private static void applyHighPerformanceGpuPreference(Path executable) { + String path = FileUtils.getAbsolutePath(executable); + try { + WinReg reg = WinReg.INSTANCE; + if (reg == null) { + return; + } + Object current = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER, + "Software\\Microsoft\\DirectX\\UserGpuPreferences", path); + if (current != null) { + LOG.info("GPU preference for %s already exists: %s".formatted(path, current)); + } else if (reg.setValue(WinReg.HKEY.HKEY_CURRENT_USER, + "Software\\Microsoft\\DirectX\\UserGpuPreferences", path, "GpuPreference=2;")) { + LOG.info("Successfully applied high performance GPU preference for java: " + path); + } else { + LOG.warning("Failed to apply high performance GPU preference for java: " + path); + } + } catch (Exception e) { + LOG.warning("Failed to apply high performance GPU preference", e); + } + } + private static final Queue> PROCESSES = new ConcurrentLinkedQueue<>(); public static int countMangedProcesses() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/RestartRequest.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/RestartRequest.java new file mode 100644 index 0000000000..33f405f931 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/RestartRequest.java @@ -0,0 +1,56 @@ +// 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 com.google.gson.JsonObject; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +/// The "please restart me" handshake between the game and the launcher. +/// +/// When the game wants to be restarted (for example after a hot update), it writes +/// [#FILE_NAME] into its run directory as `{"reason":"update"}` and exits normally. After the +/// process ends the launcher calls [#consume(Path)], deletes the file and offers to update and relaunch. +@NotNullByDefault +public final class RestartRequest { + private RestartRequest() { + } + + /// The marker file name inside the game's run directory. + public static final String FILE_NAME = "tdlm-restart-request.json"; + + /// Reads and deletes the restart request in `runDirectory`. + /// + /// A malformed file still counts as a request with reason `"unknown"`, because the file's presence is + /// the signal. The file is always removed so a request cannot trigger twice. + /// + /// @param runDirectory the game's run directory + /// @return the reason, or `null` if no request was pending + public static @Nullable String consume(Path runDirectory) { + Path file = runDirectory.resolve(FILE_NAME); + if (!Files.isRegularFile(file)) { + return null; + } + String reason = "unknown"; + try { + JsonObject object = JsonUtils.GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), JsonObject.class); + if (object != null && object.has("reason") && object.get("reason").isJsonPrimitive()) { + reason = object.get("reason").getAsString(); + } + } catch (IOException | RuntimeException ignored) { + // The presence of the file is the signal; keep the default reason. + } + try { + Files.deleteIfExists(file); + } catch (IOException ignored) { + // Nothing sensible to do; the worst case is one more prompt on the next exit. + } + return reason; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index 27c4e4bc6a..1124ca1641 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -645,9 +645,12 @@ public sealed abstract class GameSettings extends ObservableSetting { public static final String PROPERTY_HIGH_PERFORMANCE = "highPerformance"; /// Whether to launch games with high-performance GPU. + /// + /// On by default: on a laptop with an integrated and a discrete GPU, Windows otherwise picks the + /// integrated one for java. Machines with a single GPU are unaffected by the preference. @SerializedName(PROPERTY_HIGH_PERFORMANCE) private final InheritableProperty highPerformance = - newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, false); + newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, true); /// Returns the high performance GPU preference property. public InheritableProperty highPerformanceProperty() { 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 aac6ac6b40..ce19a563f9 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 @@ -398,6 +398,21 @@ public final class Instances { .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false))); } + /// Asks whether to update and relaunch after the game requested a restart (see [RestartRequest]). + /// + /// Yes runs the Start flow, which syncs the client pack and joins the server again; No leaves the + /// launcher as it is. The request file is already deleted, so the prompt shows only once. + /// + /// @param gameInstance the instance that just exited + /// @param reason the reason from the request file, for example `update` + public static void offerRestart(HMCLGameInstance gameInstance, String reason) { + Controllers.confirm( + i18n("home.restart." + ("update".equals(reason) ? "update" : "generic")), + i18n("home.restart.title"), + () -> updateInstance(gameInstance), + null); + } + /// Updates via fileApi through the Start flow — no install wizard. public static void updateInstance(HMCLGameInstance gameInstance) { gameInstance.getRepository().setSelectedInstance(gameInstance); 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 9807400fea..396a609175 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 @@ -136,7 +136,19 @@ public final class MainPage extends StackPane implements DecoratorPage { card.getStyleClass().add("home-card"); card.setAlignment(Pos.CENTER_LEFT); card.setMaxHeight(Region.USE_PREF_SIZE); - StackPane.setAlignment(card, Pos.BOTTOM_CENTER); + // Server announcement (the server's MOTD), shown above the card only when there is one. + Label motdLabel = new Label(); + motdLabel.getStyleClass().add("home-subtitle"); + motdLabel.setWrapText(true); + motdLabel.setMaxHeight(Region.USE_PREF_SIZE); + Label motdTitle = factLabel(i18n("home.motd")); + VBox motdBox = new VBox(3, motdTitle, motdLabel); + motdBox.setPadding(new Insets(0, 0, 14, 4)); + motdBox.setVisible(false); + motdBox.setManaged(false); + VBox bottom = new VBox(motdBox, card); + bottom.setMaxHeight(Region.USE_PREF_SIZE); + StackPane.setAlignment(bottom, Pos.BOTTOM_CENTER); // Start always installs/updates from fileApi then joins — never opens install or instance UI. FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch); @@ -157,7 +169,7 @@ public final class MainPage extends StackPane implements DecoratorPage { routeFact.setVisible(route != null); routeFact.setManaged(route != null); routeValue.setText(route != null ? route.name() : ""); - refreshServerStatus(route, serverFact, serverValue); + refreshServerStatus(route, serverFact, serverValue, motdBox, motdLabel); // With more than one route (for example direct and CDN), the route can be switched. List routes = ServerEndpoint.routes(currentGame.getModpackConfigurationFile()); @@ -174,7 +186,7 @@ public final class MainPage extends StackPane implements DecoratorPage { items.add(new IconedMenuItem(null, candidate.name(), () -> { ServerEndpoint.select(currentGame.getModpackConfigurationFile(), candidate); routeValue.setText(candidate.name() + " ▾"); - refreshServerStatus(candidate, serverFact, serverValue); + refreshServerStatus(candidate, serverFact, serverValue, motdBox, motdLabel); }, popup)); } list.getContent().setAll(items); @@ -183,7 +195,7 @@ public final class MainPage extends StackPane implements DecoratorPage { } }); - getChildren().addAll(art, hero, card); + getChildren().addAll(art, hero, bottom); } /// Sets the home button to install / update / join based on the selected instance and remote manifest. @@ -208,26 +220,39 @@ public final class MainPage extends StackPane implements DecoratorPage { /// Pings the server behind `route` in the background and shows the result in `serverValue`. /// /// WebSocket routes cannot be pinged with plain TCP, so the status is hidden for them. - private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue) { + private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue, + Node motdBox, Label motdLabel) { String address = route == null ? "" : route.address().trim(); boolean pingable = !address.isEmpty() && !address.toLowerCase(java.util.Locale.ROOT).startsWith("ws"); serverFact.setVisible(pingable); serverFact.setManaged(pingable); + motdBox.setVisible(false); + motdBox.setManaged(false); if (!pingable) { return; } serverValue.setText("…"); Schedulers.io().execute(() -> { String text; + String motd = ""; try { ServerAddress parsed = ServerAddress.parse(address); ServerStatusPing.Status status = ServerStatusPing.ping(parsed.host(), parsed.port() >= 0 ? parsed.port() : 25565, 3000); text = status.online() + " / " + status.max() + " · " + status.latencyMillis() + " ms"; + motd = status.motd(); } catch (IOException | IllegalArgumentException e) { text = i18n("home.server.unreachable"); } String result = text; - FXUtils.runInFX(() -> serverValue.setText(result)); + String announcement = motd; + FXUtils.runInFX(() -> { + serverValue.setText(result); + // Unreachable or empty MOTD: stay silent, the announcement area stays hidden. + boolean show = !announcement.isEmpty(); + motdLabel.setText(announcement); + motdBox.setVisible(show); + motdBox.setManaged(show); + }); }); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b4d4b2384e..c29ff38b38 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -808,6 +808,10 @@ first_run.microsoft=Sign in with Microsoft first_run.invalid=Use 1 to 16 letters, digits or underscores. home.server=Server home.server.unreachable=Unreachable +home.motd=Server announcement +home.restart.title=Restart game +home.restart.update=The game closed to apply an update. Update the client and start it again now? +home.restart.generic=The game asked to be restarted. Update the client and start it again now? server.join=Join Server server.update=Update instance.launch_and_enter_world=Play World diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index ffabf38d43..3830b278f4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -628,6 +628,10 @@ first_run.microsoft=登正版 first_run.invalid=一至十六字,限英文數字底綫。 home.server=服 home.server.unreachable=不通 +home.motd=服公告 +home.restart.title=重啟遊戲 +home.restart.update=遊戲為更新而止。今更新並重啟乎? +home.restart.generic=遊戲請重啟。今更新並重啟乎? server.join=入服 server.update=更新 # instance.launch_and_enter_world= 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 ccfe52bb9c..7627593875 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -631,6 +631,10 @@ first_run.microsoft=登录正版账号 first_run.invalid=只能用 1 到 16 个英文字母、数字或下划线。 home.server=服务器 home.server.unreachable=连接不上 +home.motd=服务器公告 +home.restart.title=重启游戏 +home.restart.update=游戏为应用更新已退出。现在更新客户端并重新启动游戏吗? +home.restart.generic=游戏请求重启。现在更新客户端并重新启动吗? server.join=进入服务器 server.update=更新 instance.launch_and_enter_world=进入世界 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 a59141ecad..089164cc9a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -625,6 +625,10 @@ first_run.microsoft=登入正版帳號 first_run.invalid=只能使用 1 到 16 個英文字母、數字或底線。 home.server=伺服器 home.server.unreachable=無法連線 +home.motd=伺服器公告 +home.restart.title=重啟遊戲 +home.restart.update=遊戲為套用更新已結束。現在更新客戶端並重新啟動遊戲嗎? +home.restart.generic=遊戲要求重啟。現在更新客戶端並重新啟動嗎? server.join=進入伺服器 server.update=更新 instance.launch_and_enter_world=進入世界 diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/game/RestartRequestTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/game/RestartRequestTest.java new file mode 100644 index 0000000000..8fb3e29f1f --- /dev/null +++ b/HMCL/src/test/java/org/jackhuang/hmcl/game/RestartRequestTest.java @@ -0,0 +1,36 @@ +// 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.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +/// Tests [RestartRequest]. +public final class RestartRequestTest { + @Test + void noFileMeansNoRequest(@TempDir Path dir) { + assertNull(RestartRequest.consume(dir)); + } + + @Test + void readsReasonAndDeletesFile(@TempDir Path dir) throws Exception { + Path file = dir.resolve(RestartRequest.FILE_NAME); + Files.writeString(file, "{\"reason\":\"update\"}"); + assertEquals("update", RestartRequest.consume(dir)); + assertFalse(Files.exists(file)); + assertNull(RestartRequest.consume(dir)); + } + + @Test + void malformedFileStillCounts(@TempDir Path dir) throws Exception { + Path file = dir.resolve(RestartRequest.FILE_NAME); + Files.writeString(file, "not json"); + assertEquals("unknown", RestartRequest.consume(dir)); + assertFalse(Files.exists(file)); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 29098c375a..b5f96565dd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -799,6 +799,10 @@ public class DefaultLauncher extends Launcher { env.put("__NV_PRIME_RENDER_OFFLOAD", "1"); env.put("__GLX_VENDOR_LIBRARY_NAME", "nvidia"); env.put("__VK_LAYER_NV_optimus", "NVIDIA_only"); + } else if (HardwareVendor.AMD.equals(discreteGraphicsCard.getVendor()) + || HardwareVendor.NVIDIA.equals(discreteGraphicsCard.getVendor())) { + // Mesa PRIME offload (open source AMD / nouveau drivers) + env.put("DRI_PRIME", "1"); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerStatusPing.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerStatusPing.java index 4c00d81676..4a8563444d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerStatusPing.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerStatusPing.java @@ -2,10 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL package org.jackhuang.hmcl.util; +import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; @@ -28,7 +30,40 @@ public final class ServerStatusPing { /// @param online the number of players online /// @param max the player limit /// @param latencyMillis the round-trip time of the ping packet in milliseconds - public record Status(int online, int max, long latencyMillis) { + /// @param motd the server's message of the day as plain text without colour codes; empty if absent + public record Status(int online, int max, long latencyMillis, String motd) { + /// Creates a status without a message of the day. + public Status(int online, int max, long latencyMillis) { + this(online, max, latencyMillis, ""); + } + } + + /// Flattens the `description` field of a status response (a plain string or a chat component with + /// `text` and `extra`) into plain text, dropping `§x` colour and format codes. + /// + /// @param description the `description` element, may be `null` + /// @return the plain text, trimmed; empty when there is nothing to show + public static String motdToPlainText(@Nullable JsonElement description) { + StringBuilder builder = new StringBuilder(); + appendComponent(description, builder); + return builder.toString().replaceAll("\u00a7.", "").strip(); + } + + private static void appendComponent(@Nullable JsonElement element, StringBuilder out) { + if (element == null || element.isJsonNull()) { + return; + } + if (element.isJsonPrimitive()) { + out.append(element.getAsString()); + } else if (element.isJsonArray()) { + for (JsonElement item : element.getAsJsonArray()) { + appendComponent(item, out); + } + } else if (element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + appendComponent(object.get("text"), out); + appendComponent(object.get("extra"), out); + } } /// The protocol number sent in the handshake; Minecraft 1.7.10 uses 5 and servers answer any value. @@ -73,9 +108,11 @@ public final class ServerStatusPing { in.readFully(json); int online; int max; + String motd; try { - JsonObject players = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class) - .getAsJsonObject("players"); + JsonObject response = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class); + motd = motdToPlainText(response.get("description")); + JsonObject players = response.getAsJsonObject("players"); online = players.get("online").getAsInt(); max = players.get("max").getAsInt(); } catch (JsonParseException | NullPointerException | ClassCastException | IllegalStateException e) { @@ -94,7 +131,7 @@ public final class ServerStatusPing { throw new IOException("Unexpected pong packet"); } in.readLong(); - return new Status(online, max, (System.nanoTime() - start) / 1_000_000); + return new Status(online, max, (System.nanoTime() - start) / 1_000_000, motd); } } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerStatusPingTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerStatusPingTest.java index 3cf1e6b793..204e4df6b4 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerStatusPingTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerStatusPingTest.java @@ -25,6 +25,7 @@ public final class ServerStatusPingTest { var status = ServerStatusPing.ping("127.0.0.1", server.getLocalPort(), 3000); assertEquals(12, status.online()); assertEquals(50, status.max()); + assertEquals("TDLM 公告: 今晚维护", status.motd()); assertTrue(status.latencyMillis() >= 0); } } @@ -45,7 +46,7 @@ public final class ServerStatusPingTest { skipPacket(in); // handshake skipPacket(in); // status request - byte[] json = "{\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8); + byte[] json = "{\"description\":{\"text\":\"§aTDLM \",\"extra\":[{\"text\":\"公告: 今晚维护\"}]},\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8); byte[] body = concat(new byte[]{0x00}, varInt(json.length), json); out.write(varInt(body.length)); out.write(body); @@ -88,4 +89,11 @@ public final class ServerStatusPingTest { for (byte[] part : parts) out.writeBytes(part); return out.toByteArray(); } + + @Test + void flattensPlainStringMotd() { + var json = com.google.gson.JsonParser.parseString("\"§l欢迎§r来玩\""); + assertEquals("欢迎来玩", ServerStatusPing.motdToPlainText(json)); + assertEquals("", ServerStatusPing.motdToPlainText(null)); + } } -- 2.47.3 From 8d1c01aed68aeeb5b5b51568813199974d3963c8 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Wed, 11 Nov 2026 14:09:45 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20lzh=20=E7=BF=BB=E8=AF=91=E7=94=A8?= =?UTF-8?q?=E5=AD=97=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- HMCL/src/main/resources/assets/lang/I18N_lzh.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 3830b278f4..76c8c218b7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -630,7 +630,7 @@ home.server=服 home.server.unreachable=不通 home.motd=服公告 home.restart.title=重啟遊戲 -home.restart.update=遊戲為更新而止。今更新並重啟乎? +home.restart.update=遊戲爲更新而止。今更新並重啟乎? home.restart.generic=遊戲請重啟。今更新並重啟乎? server.join=入服 server.update=更新 -- 2.47.3 From d02fb95bd4cae430edf17e6886834b98ad16db03 Mon Sep 17 00:00:00 2001 From: Layue13 Date: Wed, 11 Nov 2026 14:22:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20LauncherHelper=20=E7=BC=BA=20Instanc?= =?UTF-8?q?es=20=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java | 1 + 1 file changed, 1 insertion(+) 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 4185da0382..fa6e9b29af 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.setting.JavaVersionType; import org.jackhuang.hmcl.setting.LauncherVisibility; import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; -- 2.47.3