From 5eee4b49125d9203c06a23551157a99189f6fd40 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 21 Sep 2026 02:24:51 +0800 Subject: [PATCH 01/97] =?UTF-8?q?=E9=87=8D=E5=86=99=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E7=AE=A1=E7=90=86=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 2 + .../hmcl/ui/instances/GameInstancePage.java | 7 +- .../hmcl/ui/instances/Instances.java | 10 + .../hmcl/ui/instances/ServerListPage.java | 532 ++++++++++++++++++ .../resources/assets/lang/I18N.properties | 13 + .../resources/assets/lang/I18N_ar.properties | 13 + .../resources/assets/lang/I18N_de.properties | 13 + .../resources/assets/lang/I18N_es.properties | 13 + .../resources/assets/lang/I18N_ja.properties | 13 + .../resources/assets/lang/I18N_lzh.properties | 1 + .../resources/assets/lang/I18N_ru.properties | 13 + .../resources/assets/lang/I18N_uk.properties | 11 + .../assets/lang/I18N_zh_Hans.properties | 13 + .../assets/lang/I18N_zh_Hant.properties | 13 + .../org/jackhuang/hmcl/game/GameInstance.java | 7 + gradle/wrapper/gradle-wrapper.properties | 2 +- 16 files changed, 673 insertions(+), 3 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 9991ac13b85..320d275bddb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -112,6 +112,8 @@ public enum SVG { SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"), SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), + SERVERS("M 7.5 6 C 7.082031 6 6.730469 6.144531 6.4375 6.4375 C 6.144531 6.730469 6 7.082031 6 7.5 C 6 7.917969 6.144531 8.269531 6.4375 8.5625 C 6.730469 8.855469 7.082031 9 7.5 9 C 7.917969 9 8.269531 8.855469 8.5625 8.5625 C 8.855469 8.269531 9 7.917969 9 7.5 C 9 7.082031 8.855469 6.730469 8.5625 6.4375 C 8.269531 6.144531 7.917969 6 7.5 6 Z M 7.5 16 C 7.082031 16 6.730469 16.144531 6.4375 16.4375 C 6.144531 16.730469 6 17.082031 6 17.5 C 6 17.917969 6.144531 18.269531 6.4375 18.5625 C 6.730469 18.855469 7.082031 19 7.5 19 C 7.917969 19 8.269531 18.855469 8.5625 18.5625 C 8.855469 18.269531 9 17.917969 9 17.5 C 9 17.082031 8.855469 16.730469 8.5625 16.4375 C 8.269531 16.144531 7.917969 16 7.5 16 Z M 4 3 L 20 3 C 20.285156 3 20.519531 3.097656 20.710938 3.289062 C 20.902344 3.480469 21 3.714844 21 4 L 21 11 C 21 11.285156 20.902344 11.519531 20.710938 11.710938 C 20.519531 11.902344 20.285156 12 20 12 L 4 12 C 3.714844 12 3.480469 11.902344 3.289062 11.710938 C 3.097656 11.519531 3 11.285156 3 11 L 3 4 C 3 3.714844 3.097656 3.480469 3.289062 3.289062 C 3.480469 3.097656 3.714844 3 4 3 Z M 5 5 L 5 10 L 19 10 L 19 5 Z M 4 13 L 20 13 C 20.285156 13 20.519531 13.097656 20.710938 13.289062 C 20.902344 13.480469 21 13.714844 21 14 L 21 21 C 21 21.285156 20.902344 21.519531 20.710938 21.710938 C 20.519531 21.902344 20.285156 22 20 22 L 4 22 C 3.714844 22 3.480469 21.902344 3.289062 21.710938 C 3.097656 21.519531 3 21.285156 3 21 L 3 14 C 3 13.714844 3.097656 13.480469 3.289062 13.289062 C 3.480469 13.097656 3.714844 13 4 13 Z M 5 15 L 5 20 L 19 20 L 19 15 Z M 5 5 L 5 10 Z M 5 15 L 5 20 Z M 5 15"), + SERVERS_FILL("M 7.5 6 C 7.082031 6 6.730469 6.144531 6.4375 6.4375 C 6.144531 6.730469 6 7.082031 6 7.5 C 6 7.917969 6.144531 8.269531 6.4375 8.5625 C 6.730469 8.855469 7.082031 9 7.5 9 C 7.917969 9 8.269531 8.855469 8.5625 8.5625 C 8.855469 8.269531 9 7.917969 9 7.5 C 9 7.082031 8.855469 6.730469 8.5625 6.4375 C 8.269531 6.144531 7.917969 6 7.5 6 Z M 7.5 16 C 7.082031 16 6.730469 16.144531 6.4375 16.4375 C 6.144531 16.730469 6 17.082031 6 17.5 C 6 17.917969 6.144531 18.269531 6.4375 18.5625 C 6.730469 18.855469 7.082031 19 7.5 19 C 7.917969 19 8.269531 18.855469 8.5625 18.5625 C 8.855469 18.269531 9 17.917969 9 17.5 C 9 17.082031 8.855469 16.730469 8.5625 16.4375 C 8.269531 16.144531 7.917969 16 7.5 16 Z M 4 3 L 20 3 C 20.285156 3 20.519531 3.097656 20.710938 3.289062 C 20.902344 3.480469 21 3.714844 21 4 L 21 11 C 21 11.285156 20.902344 11.519531 20.710938 11.710938 C 20.519531 11.902344 20.285156 12 20 12 L 4 12 C 3.714844 12 3.480469 11.902344 3.289062 11.710938 C 3.097656 11.519531 3 11.285156 3 11 L 3 4 C 3 3.714844 3.097656 3.480469 3.289062 3.289062 C 3.480469 3.097656 3.714844 3 4 3 Z M 4 13 L 20 13 C 20.285156 13 20.519531 13.097656 20.710938 13.289062 C 20.902344 13.480469 21 13.714844 21 14 L 21 21 C 21 21.285156 20.902344 21.519531 20.710938 21.710938 C 20.519531 21.902344 20.285156 22 20 22 L 4 22 C 3.714844 22 3.480469 21.902344 3.289062 21.710938 C 3.097656 21.519531 3 21.285156 3 21 L 3 14 C 3 13.714844 3.097656 13.480469 3.289062 13.289062 C 3.480469 13.097656 3.714844 13 4 13 Z M 4 13"), SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index d53d9355f62..b32d6ed9dce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -60,6 +60,7 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); + private final TabHeader.Tab serverListTab = new TabHeader.Tab<>("serverListTab"); private final TransitionPane transitionPane = new TransitionPane(); private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty(); private final ObjectProperty instance = @@ -92,8 +93,9 @@ public GameInstancePage() { resourcePackTab.setNodeSupplier(() -> new ResourcePackListPage(instance)); worldListTab.setNodeSupplier(() -> new WorldListPage(instance)); schematicsTab.setNodeSupplier(() -> new SchematicsPage(instance)); + serverListTab.setNodeSupplier(() -> new ServerListPage(instance)); - tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); + tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab, serverListTab); tab.select(gameSettingsTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); @@ -356,7 +358,8 @@ protected Skin(GameInstancePage control) { .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) - .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); + .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL) + .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("servers.manage"), SVG.SERVERS, SVG.SERVERS_FILL); VBox.setVgrow(sideBar, Priority.ALWAYS); PopupMenu browseList = new PopupMenu(); 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 8d5b2ed5394..edeccfa29d3 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 @@ -384,6 +384,16 @@ public static void generateLaunchScriptForQuickEnterWorld(HMCLGameInstance gameI launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName))); } + public static void launchAndEnterServer(HMCLGameInstance gameInstance, String serverIp) { + launch(gameInstance, launcherHelper -> + launcherHelper.setQuickPlayOption(new QuickPlayOption.MultiPlayer(serverIp))); + } + + public static void generateLaunchScriptForQuickConnectServer(HMCLGameInstance gameInstance, String serverIp) { + generateLaunchScript(gameInstance, launcherHelper -> + launcherHelper.setQuickPlayOption(new QuickPlayOption.MultiPlayer(serverIp))); + } + private static void ensureSelectedAccount(Consumer action) { Account account = Accounts.getSelectedAccount(); if (SettingsManager.isNewlyCreated() && !AuthlibInjectorServers.getServers().isEmpty() && diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java new file mode 100644 index 00000000000..307d51bc88b --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java @@ -0,0 +1,532 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXCheckBox; +import com.jfoenix.controls.JFXListView; +import com.jfoenix.controls.JFXPopup; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.Skin; +import javafx.scene.control.Tooltip; +import javafx.scene.image.Image; +import javafx.scene.input.MouseButton; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.glavo.nbt.io.MinecraftEdition; +import org.glavo.nbt.io.NBTCodec; +import org.glavo.nbt.tag.*; +import org.jackhuang.hmcl.game.GameInstance; +import org.jackhuang.hmcl.game.HMCLGameInstance; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.*; +import org.jackhuang.hmcl.ui.construct.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; +import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ServerListPage extends ListPageBase { + private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); + private final BooleanProperty showHide = new SimpleBooleanProperty(this, "showHide", false); + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + + private @Nullable HMCLGameInstance gameInstance; + private List serverHolders; + + private int refreshCount = 0; + + public ServerListPage(ObservableValue instanceContext) { + Objects.requireNonNull(instanceContext, "instanceContext"); + + showAll.addListener(e -> updateServerList()); + showHide.addListener(e -> updateServerList()); + + listenerHolder.add(FXUtils.onWeakChangeAndOperate(instanceContext, current -> { + if (current != null) { + loadInstance(current); + } + })); + } + + @Override + protected Skin createDefaultSkin() { + return new ServerListPageSkin(); + } + + public void launchAndEnterServer(Server server) { + if (gameInstance != null) { + Instances.launchAndEnterServer(gameInstance, server.ip); + } + } + + public void copyServerIp(Server server) { + FXUtils.copyText(server.ip, i18n("servers.manage.copy.server.ip.ok.toast")); + } + + public void delete(Server server) { + Controllers.confirm( + i18n("button.remove.confirm"), + i18n("server.delete"), + () -> Task.runAsync(() -> { + ServerHolder holder = server.holder; + if (holder == null) return; + + Task.runAsync(Schedulers.io(), () -> { + List servers = Server.getServers(holder.fromServersDatFilePath); + servers.remove(server); + Server.saveServerToDat(holder.fromServersDatFilePath, servers); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) + LOG.warning("Failed to save server data.", exception); + + refresh(); + }).start(); + + }).start(), + + null + ); + } + + public void copyToInstance(Server server) { + ServerHolder holder = server.holder; + if (holder == null) return; + if (gameInstance == null) return; + + Task.runAsync(Schedulers.io(), () -> { + List servers = Server.getServers(gameInstance.getServersDatFilePath()); + servers.add(server); + Server.saveServerToDat(gameInstance.getServersDatFilePath(), servers); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) + LOG.warning("Failed to save server data.", exception); + + refresh(); + }).start(); + } + + public void generateLaunchScript(Server server) { + if (gameInstance != null) { + Instances.generateLaunchScriptForQuickConnectServer(gameInstance, server.ip); + } + } + + public void loadInstance(HMCLGameInstance.Optional instance) { + this.gameInstance = instance.instance(); + refresh(); + } + + private void updateServerList() { + if (serverHolders == null || gameInstance == null) { + getItems().clear(); + return; + } + + var stream = serverHolders.stream(); + if (!showAll.get()) { + stream = stream.filter(holder -> holder.fromServersDatFilePath.equals(gameInstance.getServersDatFilePath())); + } + if (!showHide.get()) { + stream = stream.filter(holder -> !holder.server.hidden); + } + + getItems().setAll(stream.toList()); + } + + private void refresh() { + if (gameInstance == null) + return; + + int currentRefresh = ++refreshCount; + HMCLGameInstance gameInstance = this.gameInstance; + + setLoading(true); + Task.supplyAsync(Schedulers.io(), () -> { + Map> pathListMap = new LinkedHashMap<>(); + for (HMCLGameInstance instance : gameInstance.getRepository().getSnapshot().getInstances()) { + if (pathListMap.containsKey(instance.getServersDatFilePath())) { + for (ServerHolder holder : pathListMap.get(instance.getServersDatFilePath())) { + holder.holdInstances.add(instance); + } + } else { + ArrayList holders = new ArrayList<>(); + pathListMap.put(instance.getServersDatFilePath(), holders); + + List parsedServers = Server.getServers(instance.getServersDatFilePath()); + for (int index = 0; index < parsedServers.size(); index++) { + Server server = parsedServers.get(index); + ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), index, server); + server.holder = holder; + holder.holdInstances.add(instance); + + holders.add(holder); + } + } + } + + + return pathListMap.values().stream().flatMap(Collection::stream).toList(); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (refreshCount != currentRefresh) { + // A newer refresh task is running, discard this result + return; + } + + serverHolders = result; + updateServerList(); + + if (exception != null) + LOG.warning("Failed to load server list page", exception); + + setLoading(false); + }).start(); + } + + private static final class ServerListCell extends ListCell { + + private final ServerListPage page; + + private final RipplerContainer graphic; + private final ImageContainer imageView; + private final Tooltip leftTooltip; + private final TwoLineListItem content; + private final JFXButton btnLaunch; + + public ServerListCell(ServerListPage page) { + this.page = page; + + BorderPane root = new BorderPane(); + root.getStyleClass().add("md-list-cell"); + root.setPadding(new Insets(8)); + + { + StackPane left = new StackPane(); + this.leftTooltip = new Tooltip(); + FXUtils.installSlowTooltip(left, leftTooltip); + root.setLeft(left); + left.setPadding(new Insets(0, 8, 0, 0)); + + this.imageView = new ImageContainer(32); + left.getChildren().add(imageView); + } + + { + this.content = new TwoLineListItem(); + root.setCenter(content); + content.setMouseTransparent(true); + } + + { + HBox right = new HBox(8); + root.setRight(right); + right.setAlignment(Pos.CENTER_RIGHT); + + btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); + btnLaunch.managedProperty().bind(btnLaunch.visibleProperty()); + right.getChildren().add(btnLaunch); + FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch")); + btnLaunch.setOnAction(event -> { + ServerHolder holder = getItem(); + if (holder != null) + page.launchAndEnterServer(holder.server); + }); + + JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); + right.getChildren().add(btnMore); + btnMore.setOnAction(event -> { + ServerHolder holder = getItem(); + if (holder != null) + showPopupMenu(holder, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); + }); + } + + this.graphic = new RipplerContainer(root); + graphic.setOnMouseClicked(event -> { + if (event.getClickCount() != 1) + return; + + ServerHolder holder = getItem(); + if (holder == null) + return; + + if (event.getButton() == MouseButton.SECONDARY) + showPopupMenu(holder, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); + }); + } + + @Override + protected void updateItem(ServerHolder holder, boolean empty) { + ServerHolder oldHolder = getItem(); + boolean oldEmpty = isEmpty(); + + super.updateItem(holder, empty); + + if (oldHolder == holder && oldEmpty == empty) return; + + this.graphic.releaseRippleImmediately(); + this.content.getTags().clear(); + + if (empty || holder == null) { + setGraphic(null); + imageView.setImage(null); + leftTooltip.setText(""); + content.setTitle(""); + content.setSubtitle(""); + } else { + imageView.setImage(holder.server.iconImage == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : holder.server.iconImage); + leftTooltip.setText(holder.server.ip); + content.setTitle(holder.server.name != null ? parseColorEscapes(holder.server.name) : ""); + content.setSubtitle(holder.server.ip); + + if (holder.server.hidden) { + content.addTag(i18n("server.tag.hide")); + } + + if (holder.holdInstances.contains(page.gameInstance)) { + content.addTag(i18n("server.tag.hold.current")); + holder.holdInstances.stream() + .filter(e -> !e.equals(page.gameInstance)) + .map(gameInstance -> gameInstance.getId().id()) + .forEach(content::addTag); + } else { + holder.holdInstances.stream() + .map(gameInstance -> gameInstance.getId().id()) + .forEach(content::addTag); + } + + setGraphic(graphic); + } + } + + // Popup Menu + + public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { + PopupMenu popupMenu = new PopupMenu(); + JFXPopup popup = new JFXPopup(popupMenu); + + IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder.server), popup); + popupMenu.getContent().addAll( + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_connect_server"), () -> + page.launchAndEnterServer(holder.server), popup + ), + new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> + page.generateLaunchScript(holder.server), popup + ), + new MenuSeparator(), + new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.server.ip"), () -> + page.copyServerIp(holder.server), popup + ), + new MenuSeparator(), + copyToInstanceMEnuItem, + new IconedMenuItem(SVG.DELETE_FOREVER, i18n("server.delete"), () -> + page.delete(holder.server), popup + ) + ); + if (page.gameInstance != null) { + copyToInstanceMEnuItem.setDisable(getItem().fromServersDatFilePath.equals(page.gameInstance.getServersDatFilePath())); + } + + JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup); + popup.show(this, vPosition, hPosition, initOffsetX, vPosition == JFXPopup.PopupVPosition.TOP ? initOffsetY : -initOffsetY); + } + } + + public static final class ServerHolder { + final @NotNull List holdInstances = new ArrayList<>(); + final @NotNull Path fromServersDatFilePath; + final int inDatPathSlot; + final @NotNull Server server; + + public ServerHolder(@NotNull Path fromServersDatFilePath, int inDatPathSlot, @NotNull Server server) { + this.fromServersDatFilePath = fromServersDatFilePath; + this.inDatPathSlot = inDatPathSlot; + this.server = server; + } + } + + public static final class Server { + final boolean acceptTextures; + final boolean hidden; + final @Nullable String icon; + final @Nullable String ip; + final @Nullable String name; + + transient final Image iconImage; + transient ServerHolder holder; + + public Server( + boolean acceptTextures, + boolean hidden, + @Nullable String icon, + @Nullable String ip, + @Nullable String name + ) { + this.acceptTextures = acceptTextures; + this.hidden = hidden; + this.icon = icon; + this.ip = ip; + this.name = name; + + this.iconImage = parseImage(icon); + } + + public static Image parseImage(String imageBase64String) { + if (imageBase64String == null || imageBase64String.isEmpty()) { + return null; + } + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { + // png format. + return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); + } catch (Exception e) { + LOG.warning("Failed to decode server icon", e); + return null; + } + } + + public static List getServers(@NotNull Path serversDatFilePath) { + List set = new ArrayList<>(); + if (Files.exists(serversDatFilePath)) { + try { + CompoundTag tags = (CompoundTag) NBTCodec.of(MinecraftEdition.JAVA_EDITION).readTag(serversDatFilePath); + Tag serversTag = tags.get("servers"); + if (serversTag instanceof ListTag st) { + for (Tag tag : st) { + if (tag instanceof CompoundTag cTag) { + set.add(new Server( + cTag.get("acceptTextures") instanceof ByteTag bt && bt.getValue() != 0, + cTag.get("hidden") instanceof ByteTag bt && bt.getValue() != 0, + cTag.get("icon") instanceof StringTag stg ? stg.getValue() : null, + cTag.get("ip") instanceof StringTag stg ? stg.getValue() : null, + cTag.get("name") instanceof StringTag stg ? stg.getValue() : null + )); + } + } + } + } catch (IOException e) { + LOG.error("Failed to read servers.dat file.", e); + } + } + return set; + } + + public static void saveServerToDat(Path datFile, List saveServerData) { + ListTag tag = new ListTag<>(); + for (Server server : saveServerData) { + CompoundTag serverTag = new CompoundTag(); + server.writeToCompoundTag(serverTag); + tag.addTag(serverTag); + } + + CompoundTag root = new CompoundTag(); + root.addTag("servers", tag); + + try (var output = Files.newOutputStream(datFile)) { + NBTCodec.of(MinecraftEdition.JAVA_EDITION).writeTag(output, root); + } catch (IOException e) { + LOG.error("Failed to write servers.dat file.", e); + } + } + + public void writeToCompoundTag(CompoundTag tag) { + tag.addByte("acceptTextures", (byte) (acceptTextures ? 1 : 0)); + tag.addByte("hidden", (byte) (hidden ? 1 : 0)); + if (icon != null) tag.addString("icon", icon); + if (ip != null) tag.addString("ip", ip); + if (name != null) tag.addString("name", name); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Server that = (Server) o; + return Objects.equals(ip, that.ip) && + Objects.equals(name, that.name) && + Objects.equals(acceptTextures, that.acceptTextures) && + Objects.equals(hidden, that.hidden) && + Objects.equals(icon, that.icon); + } + + @Override + public int hashCode() { + return Objects.hash(ip, name); + } + + @Override + public String toString() { + return "Server{" + + "acceptTextures=" + acceptTextures + + ", hidden=" + hidden + + ", icon='" + icon + '\'' + + ", ip='" + ip + '\'' + + ", name='" + name + '\'' + + '}'; + } + } + + private final class ServerListPageSkin extends ToolbarListPageSkin { + + ServerListPageSkin() { + super(ServerListPage.this); + + StackPane placeholderContainer = new StackPane(); + placeholderContainer.getStyleClass().add("notice-pane"); + Label placeholderLabel = new Label(i18n("server.empty")); + placeholderContainer.getChildren().add(placeholderLabel); + listView.setPlaceholder(placeholderContainer); + } + + @Override + protected List initializeToolbar(ServerListPage skinnable) { + JFXCheckBox chkShowAll = new JFXCheckBox(i18n("servers.show_all")); + chkShowAll.selectedProperty().bindBidirectional(skinnable.showAll); + + JFXCheckBox chkShowHide = new JFXCheckBox(i18n("servers.show_hide")); + chkShowHide.selectedProperty().bindBidirectional(skinnable.showHide); + + return Arrays.asList( + chkShowAll, + chkShowHide, + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + ); + } + + @Override + protected ListCell createListCell(JFXListView listView) { + return new ServerListCell(getSkinnable()); + } + } +} diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9a56febc90e..b0a66e54eb7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -808,6 +808,7 @@ instance.game.support_status.untested=Untested instance.game.type=Type instance.launch=Launch Game instance.launch_and_enter_world=Play World +instance.launch_and_connect_server=Play Server instance.launch.empty=Start Game instance.launch.empty.installing=Installing Game instance.launch.empty.tooltip=Install and launch the latest official release @@ -1820,3 +1821,15 @@ wizard.prev=< Prev wizard.failed=Failed wizard.finish=Finish wizard.next=Next > + +servers.manage=Servers +servers.show_all=Show All +servers.show_hide=Show Hidden +servers.manage.copy.server.ip=Copy IP address +servers.manage.copy.server.ip.ok.toast=Server IP address has been copied to clipboard +servers.manage.copy.to.instance=Copy to current instance +servers.manage.delete=Delete +server.delete=Delete this server +server.tag.hide=Hidden +server.tag.hold.current=Current +server.empty=No Server \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 635db716b65..7eca8246246 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -809,6 +809,7 @@ instance.game.support_status.untested=غير مختبر instance.game.type=النوع instance.launch=تشغيل اللعبة instance.launch_and_enter_world=لعب في العالم +instance.launch_and_connect_server=لعب خوادم instance.launch.empty=تشغيل اللعبة instance.launch.empty.installing=جارٍ تثبيت اللعبة instance.launch.empty.tooltip=تثبيت وتشغيل أحدث إصدار رسمي @@ -1817,3 +1818,15 @@ wizard.prev=< السابق wizard.failed=فشل wizard.finish=إنهاء wizard.next=التالي > + +servers.manage=خوادم +servers.show_all=عرض الكل +servers.show_hide=إظهار الإخفاء +servers.manage.copy.server.ip=نسخ عنوان IP +servers.manage.copy.server.ip.ok.toast=تم نسخ عنوان خادم IP إلى الحافظة +servers.manage.copy.to.instance=نسخ إلى هذه الحالة +servers.manage.delete=حذف +server.delete=حذف هذا الخادم +server.tag.hide=مخفي +server.tag.hold.current=المحمل +# server.empty= \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 0e3534cb791..2d11abcef3f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -807,6 +807,7 @@ instance.game.support_status.untested=Ungetestet instance.game.type=Typ instance.launch=Spiel starten instance.launch_and_enter_world=Welt spielen +instance.launch_and_connect_server=Server spielen instance.launch.empty=Spiel starten instance.launch.empty.installing=Installiere Spiel instance.launch.empty.tooltip=Die neueste offizielle Release installieren und starten @@ -1819,3 +1820,15 @@ wizard.prev=< Zurück wizard.failed=Fehlgeschlagen wizard.finish=Fertigstellen wizard.next=Weiter > + +servers.manage=Servers +servers.show_all=Alle anzeigen +servers.show_hide=Versteckt anzeigen +servers.manage.copy.server.ip=IP adresse kopieren +servers.manage.copy.server.ip.ok.toast=Die IP addresse des Servers wurde in die Zwischenablage kopiert +servers.manage.copy.to.instance=Auf die aktuelle Instanz kopieren +servers.manage.delete=Löschen +server.delete=Diesen Server löschen +server.tag.hide=Versteckt +server.tag.hold.current=Aktuell +server.empty=Kein Server \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 38d32da099e..b9a2604eb69 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -801,6 +801,7 @@ instance.game.snapshots=Snapshots instance.game.type=Tipo instance.launch=Iniciar Minecraft # instance.launch_and_enter_world= +# instance.launch_and_connect_server= instance.launch.empty=Iniciar Minecraft instance.launch.empty.installing=Instalando el juego instance.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego. @@ -1803,3 +1804,15 @@ wizard.prev=< Previo wizard.failed=Falló wizard.finish=Finalizar wizard.next=Siguiente > + +servers.manage=Servidores +servers.show_all=Mostrar todo +servers.show_hide=Mostrar Oculto +servers.manage.copy.server.ip=Copiar dirección IP +servers.manage.copy.server.ip.ok.toast=Se ha copiado la dirección IP del servidor al portapapeles +servers.manage.copy.to.instance=Copiar a esta instancia +servers.manage.delete=Eliminar +server.delete=Eliminar este servidor +server.tag.hide=Oculto +server.tag.hold.current=PoseídoActual +# server.empty= \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index d6f3e2fc5df..5ac7894b5c1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -611,6 +611,7 @@ instance.game.snapshots=Snapshots # instance.game.type= instance.launch=プレイ # instance.launch_and_enter_world= +# instance.launch_and_connect_server= # instance.launch.empty= # instance.launch.empty.installing= # instance.launch.empty.tooltip= @@ -1605,3 +1606,15 @@ wizard.prev=< 前へ wizard.failed=失敗 wizard.finish=終了 wizard.next=次へ > + +servers.manage=サーバー +servers.show_all=全て表示 +servers.show_hide=ヒョウジヒヒョウジ +servers.manage.copy.server.ip=IP アドレスをコピー +servers.manage.copy.server.ip.ok.toast=サーバー IP アドレスをクリップボードにコピーしました +servers.manage.copy.to.instance=このインスタンスにコピー +servers.manage.delete=削除 +server.delete=このサーバーを削除 +server.tag.hide=隠しの +server.tag.hold.current=保有中 +# server.empty= \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 0abaf77bbf6..ef42a1c2f6f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -623,6 +623,7 @@ instance.game.snapshots=預版 instance.game.type=版類 instance.launch=戲 # instance.launch_and_enter_world= +# instance.launch_and_connect_server= instance.launch.empty=啟戲 instance.launch.empty.installing=裝戲 instance.launch.empty.tooltip=裝啟至新之當版 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 8b75ae658b8..a3554ea7a14 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -738,6 +738,7 @@ instance.game.support_status.untested=Не проверено instance.game.type=Тип instance.launch=Играть instance.launch_and_enter_world=Играть в этом мире +instance.launch_and_connect_server=Играть в этом сервере instance.launch.empty=Играть instance.launch.empty.installing=Установка игры instance.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры. @@ -1737,3 +1738,15 @@ wizard.prev=< Пред. wizard.failed=Не удалось wizard.finish=Готово wizard.next=След. > + +servers.manage=Серверы +servers.show_all=Показать все +servers.show_hide=Показать Скрытое +servers.manage.copy.server.ip=Копировать IP-адрес +servers.manage.copy.server.ip.ok.toast=IP-адрес сервера скопирован в буфер обмена +servers.manage.copy.to.instance=Копировать в этот экземпляр +servers.manage.delete=Удалить +server.delete=Удалить этот сервер +server.tag.hide=Скрытое +server.tag.hold.current=ВладёныйТекущий +server.empty=Нет Серверы \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 6f1985bff05..1c293a0c607 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -762,6 +762,7 @@ instance.game.support_status.untested=Нетестована instance.game.type=Тип instance.launch=Запустити гру instance.launch_and_enter_world=Грати у світ +instance.launch_and_connect_server=Грати у сервері instance.launch.empty=Почати гру instance.launch.empty.installing=Встановлення гри instance.launch.empty.tooltip=Встановити та запустити останній офіційний реліз @@ -1768,3 +1769,13 @@ wizard.prev=< Попередній wizard.failed=Не вдалося wizard.finish=Завершити wizard.next=Наступний > + +servers.manage=Сервери +servers.show_all=Показати все +servers.show_hide=Показати Приховане +servers.manage.copy.server.ip=Копіювати IP-адресу +servers.manage.copy.server.ip.ok.toast=IP-адреса сервера скопійована в буфер обміну +server.delete=Видалити цей сервер +server.tag.hide=Приховане +server.tag.hold.current=ВласнийПоточний +server.empty=Немає Сервери \ No newline at end of file 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 be22b30fc38..48f4b59c66a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -626,6 +626,7 @@ instance.game.support_status.untested=未经测试 instance.game.type=版本类型 instance.launch=启动游戏 instance.launch_and_enter_world=进入世界 +instance.launch_and_connect_server=进入服务器 instance.launch.empty=开始游戏 instance.launch.empty.installing=安装游戏 instance.launch.empty.tooltip=安装并启动最新正式版游戏 @@ -1626,3 +1627,15 @@ wizard.prev=< 上一步 wizard.failed=失败 wizard.finish=完成 wizard.next=下一步 > + +servers.manage=服务器管理 +servers.show_all=显示全部 +servers.show_hide=显示隐藏 +servers.manage.copy.server.ip=复制地址 +servers.manage.copy.server.ip.ok.toast=已将服务器地址复制到剪贴板 +servers.manage.copy.to.instance=复制到此实例 +servers.manage.delete=删除 +server.delete=删除此服务器 +server.tag.hide=隐藏 +server.tag.hold.current=当前实例 +server.empty=没有服务器 \ No newline at end of file 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 458f32c556f..59cf8626e37 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -620,6 +620,7 @@ instance.game.support_status.untested=未經測試 instance.game.type=版本類型 instance.launch=啟動遊戲 instance.launch_and_enter_world=進入世界 +instance.launch_and_connect_server=進入伺服器 instance.launch.empty=開始遊戲 instance.launch.empty.installing=安裝遊戲 instance.launch.empty.tooltip=安裝並啟動最新正式版遊戲 @@ -1620,3 +1621,15 @@ wizard.prev=< 上一步 wizard.failed=失敗 wizard.finish=完成 wizard.next=下一步 > + +servers.manage=伺服器管理 +servers.show_all=顯示全部 +servers.show_hide=顯示隱藏 +servers.manage.copy.server.ip=複製位址 +servers.manage.copy.server.ip.ok.toast=已將伺服器位址複製到剪貼簿 +servers.manage.copy.to.instance=複製到此實例 +servers.manage.delete=刪除 +server.delete=刪除此伺服器 +server.tag.hide=隱藏 +server.tag.hold.current=目前實例 +server.empty=沒有伺服器 \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java index 0b6a6f88ae8..d32c67a9d06 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstance.java @@ -148,6 +148,13 @@ default Path getResourcePackDirectory() { return getRunDirectory().resolve("resourcepacks"); } + /// Returns the server dat file held by this instance. + /// + /// @return the server dat file below the held + default Path getServersDatFilePath() { + return getRunDirectory().resolve("servers.dat"); + } + /// Returns the directory containing saved worlds used by this instance. /// /// @return the saves directory below the run directory diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8b827a4527d..ffeb9d29282 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.1-bin.zip distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a networkTimeout=10000 retries=0 From 008f783d71120d23c51008024fb85a29e79e238a Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 21 Sep 2026 20:50:07 +0800 Subject: [PATCH 02/97] =?UTF-8?q?=E8=BF=98=E5=8E=9FdistributionUrl,=20?= =?UTF-8?q?=E6=9B=B4=E6=94=B9=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=AE=A1=E7=90=86?= =?UTF-8?q?tab=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java | 2 -- .../java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 320d275bddb..9991ac13b85 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -112,8 +112,6 @@ public enum SVG { SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"), SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), - SERVERS("M 7.5 6 C 7.082031 6 6.730469 6.144531 6.4375 6.4375 C 6.144531 6.730469 6 7.082031 6 7.5 C 6 7.917969 6.144531 8.269531 6.4375 8.5625 C 6.730469 8.855469 7.082031 9 7.5 9 C 7.917969 9 8.269531 8.855469 8.5625 8.5625 C 8.855469 8.269531 9 7.917969 9 7.5 C 9 7.082031 8.855469 6.730469 8.5625 6.4375 C 8.269531 6.144531 7.917969 6 7.5 6 Z M 7.5 16 C 7.082031 16 6.730469 16.144531 6.4375 16.4375 C 6.144531 16.730469 6 17.082031 6 17.5 C 6 17.917969 6.144531 18.269531 6.4375 18.5625 C 6.730469 18.855469 7.082031 19 7.5 19 C 7.917969 19 8.269531 18.855469 8.5625 18.5625 C 8.855469 18.269531 9 17.917969 9 17.5 C 9 17.082031 8.855469 16.730469 8.5625 16.4375 C 8.269531 16.144531 7.917969 16 7.5 16 Z M 4 3 L 20 3 C 20.285156 3 20.519531 3.097656 20.710938 3.289062 C 20.902344 3.480469 21 3.714844 21 4 L 21 11 C 21 11.285156 20.902344 11.519531 20.710938 11.710938 C 20.519531 11.902344 20.285156 12 20 12 L 4 12 C 3.714844 12 3.480469 11.902344 3.289062 11.710938 C 3.097656 11.519531 3 11.285156 3 11 L 3 4 C 3 3.714844 3.097656 3.480469 3.289062 3.289062 C 3.480469 3.097656 3.714844 3 4 3 Z M 5 5 L 5 10 L 19 10 L 19 5 Z M 4 13 L 20 13 C 20.285156 13 20.519531 13.097656 20.710938 13.289062 C 20.902344 13.480469 21 13.714844 21 14 L 21 21 C 21 21.285156 20.902344 21.519531 20.710938 21.710938 C 20.519531 21.902344 20.285156 22 20 22 L 4 22 C 3.714844 22 3.480469 21.902344 3.289062 21.710938 C 3.097656 21.519531 3 21.285156 3 21 L 3 14 C 3 13.714844 3.097656 13.480469 3.289062 13.289062 C 3.480469 13.097656 3.714844 13 4 13 Z M 5 15 L 5 20 L 19 20 L 19 15 Z M 5 5 L 5 10 Z M 5 15 L 5 20 Z M 5 15"), - SERVERS_FILL("M 7.5 6 C 7.082031 6 6.730469 6.144531 6.4375 6.4375 C 6.144531 6.730469 6 7.082031 6 7.5 C 6 7.917969 6.144531 8.269531 6.4375 8.5625 C 6.730469 8.855469 7.082031 9 7.5 9 C 7.917969 9 8.269531 8.855469 8.5625 8.5625 C 8.855469 8.269531 9 7.917969 9 7.5 C 9 7.082031 8.855469 6.730469 8.5625 6.4375 C 8.269531 6.144531 7.917969 6 7.5 6 Z M 7.5 16 C 7.082031 16 6.730469 16.144531 6.4375 16.4375 C 6.144531 16.730469 6 17.082031 6 17.5 C 6 17.917969 6.144531 18.269531 6.4375 18.5625 C 6.730469 18.855469 7.082031 19 7.5 19 C 7.917969 19 8.269531 18.855469 8.5625 18.5625 C 8.855469 18.269531 9 17.917969 9 17.5 C 9 17.082031 8.855469 16.730469 8.5625 16.4375 C 8.269531 16.144531 7.917969 16 7.5 16 Z M 4 3 L 20 3 C 20.285156 3 20.519531 3.097656 20.710938 3.289062 C 20.902344 3.480469 21 3.714844 21 4 L 21 11 C 21 11.285156 20.902344 11.519531 20.710938 11.710938 C 20.519531 11.902344 20.285156 12 20 12 L 4 12 C 3.714844 12 3.480469 11.902344 3.289062 11.710938 C 3.097656 11.519531 3 11.285156 3 11 L 3 4 C 3 3.714844 3.097656 3.480469 3.289062 3.289062 C 3.480469 3.097656 3.714844 3 4 3 Z M 4 13 L 20 13 C 20.285156 13 20.519531 13.097656 20.710938 13.289062 C 20.902344 13.480469 21 13.714844 21 14 L 21 21 C 21 21.285156 20.902344 21.519531 20.710938 21.710938 C 20.519531 21.902344 20.285156 22 20 22 L 4 22 C 3.714844 22 3.480469 21.902344 3.289062 21.710938 C 3.097656 21.519531 3 21.285156 3 21 L 3 14 C 3 13.714844 3.097656 13.480469 3.289062 13.289062 C 3.480469 13.097656 3.714844 13 4 13 Z M 4 13"), SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index b32d6ed9dce..fb9d27e6e87 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -359,7 +359,7 @@ protected Skin(GameInstancePage control) { .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL) - .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("servers.manage"), SVG.SERVERS, SVG.SERVERS_FILL); + .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("servers.manage"), SVG.DRESSER); VBox.setVgrow(sideBar, Priority.ALWAYS); PopupMenu browseList = new PopupMenu(); diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffeb9d29282..8b827a4527d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a networkTimeout=10000 retries=0 From 3a3cbc05bc6ae8cd993c3d4cdf54b537cf1737bb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 23 Sep 2026 00:12:10 +0800 Subject: [PATCH 03/97] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E6=B7=BB=E5=8A=A0=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/GameInstancePage.java | 1 + .../ui/instances/server/AddServerPane.java | 156 +++++++++++ .../{ => server}/ServerListPage.java | 248 ++++++------------ .../assets/lang/I18N_zh_Hans.properties | 8 +- .../assets/lang/I18N_zh_Hant.properties | 8 +- .../org/jackhuang/hmcl/server/Server.java | 133 ++++++++++ .../org/jackhuang/hmcl/util/NBTUtils.java | 7 + 7 files changed, 396 insertions(+), 165 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java rename HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/{ => server}/ServerListPage.java (65%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index fb9d27e6e87..740030826f9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.game.GameSettingsPage; +import org.jackhuang.hmcl.ui.instances.server.ServerListPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java new file mode 100644 index 00000000000..056995b66a0 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java @@ -0,0 +1,156 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances.server; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; +import com.jfoenix.controls.JFXTextField; +import javafx.beans.binding.BooleanBinding; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.DialogAware; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.RequiredValidator; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; + +import java.util.function.Consumer; + +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.ui.FXUtils.setValidateWhileTextChanged; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class AddServerPane extends TransitionPane implements DialogAware { + private final Runnable failed; + private final Consumer success; + + private final GridPane body; + private final JFXTextField txtServerName; + private final JFXTextField txtServerIP; + + public AddServerPane(Runnable failed, Consumer success) { + this.failed = failed; + this.success = success; + + getStyleClass().add("skin-pane"); + + JFXDialogLayout rootLayout = new JFXDialogLayout(); + getChildren().setAll(rootLayout); + rootLayout.setHeading(new Label(i18n("servers.manager.add.head"))); + + VBox bodyVbox = new VBox(); + bodyVbox.setSpacing(8); + body = new GridPane(); + + VBox.setMargin(body, new Insets(20, 0, 0, 0)); + body.setVgap(22); + body.setHgap(15); + body.setAlignment(Pos.CENTER); + + ColumnConstraints col0 = new ColumnConstraints(); + col0.setMinWidth(USE_PREF_SIZE); + body.getColumnConstraints().add(col0); + ColumnConstraints col1 = new ColumnConstraints(); + col1.setHgrow(Priority.ALWAYS); + body.getColumnConstraints().add(col1); + + // server name + { + Label label = new Label(); + label.setText(i18n("servers.manager.add.server.name")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 0); + + txtServerName = new JFXTextField(); + txtServerName.setPromptText(i18n("servers.manager.add.server.name.def")); + body.add(txtServerName, 1, 0); + } + + // server ip + { + Label label = new Label(); + label.setText(i18n("servers.manager.add.server.ip")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 1); + + txtServerIP = new JFXTextField(); + txtServerIP.setValidators(new RequiredValidator()); + setValidateWhileTextChanged(txtServerIP, true); + body.add(txtServerIP, 1, 1); + } + + bodyVbox.getChildren().add(body); + rootLayout.setBody(bodyVbox); + + JFXButton btnAccept = new JFXButton(i18n("servers.manager.add.accept")); + btnAccept.getStyleClass().add("dialog-accept"); + btnAccept.setOnAction(e -> onAdd()); + btnAccept.disableProperty().bind(new BooleanBinding() { + { + bind(txtServerIP.textProperty()); + } + + @Override + protected boolean computeValue() { + return !txtServerIP.validate(); + } + }); + + JFXButton btnCancel = new JFXButton(i18n("button.cancel")); + btnCancel.getStyleClass().add("dialog-cancel"); + btnCancel.setOnAction(e -> onCancel()); + onEscPressed(this, btnCancel::fire); + + SpinnerPane spinner = new SpinnerPane(); + spinner.getStyleClass().add("small-spinner-pane"); + spinner.setContent(btnAccept); + + rootLayout.setActions(btnAccept, btnCancel); + } + + private void onCancel() { + fireEvent(new DialogCloseEvent()); + failed.run(); + } + + private void onAdd() { + String serverName = txtServerName.getText(); + String serverIP = txtServerIP.getText(); + if (serverIP == null) return; + if (serverName == null || serverName.isEmpty()) { + serverName = i18n("servers.manager.add.server.name.def"); + } + + body.setDisable(true); + + fireEvent(new DialogCloseEvent()); + // todo online check + success.accept(new ServerListPage.IconedServer(true, false, null, serverName, serverIP)); + } + + @Override + public void onDialogShown() { + txtServerName.requestFocus(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java similarity index 65% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 307d51bc88b..6705e6cf94e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.instances; +package org.jackhuang.hmcl.ui.instances.server; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; @@ -36,25 +36,24 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; -import org.glavo.nbt.io.MinecraftEdition; -import org.glavo.nbt.io.NBTCodec; -import org.glavo.nbt.tag.*; import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.game.HMCLGameInstance; +import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; -import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; +import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -87,50 +86,46 @@ protected Skin createDefaultSkin() { return new ServerListPageSkin(); } - public void launchAndEnterServer(Server server) { + public void launchAndEnterServer(ServerHolder holder) { if (gameInstance != null) { - Instances.launchAndEnterServer(gameInstance, server.ip); + Instances.launchAndEnterServer(gameInstance, holder.server.getIp()); } } - public void copyServerIp(Server server) { - FXUtils.copyText(server.ip, i18n("servers.manage.copy.server.ip.ok.toast")); + public void copyServerIp(ServerHolder holder) { + FXUtils.copyText(holder.server.getIp(), i18n("servers.manage.copy.server.ip.ok.toast")); } - public void delete(Server server) { + public void delete(ServerHolder holder) { Controllers.confirm( i18n("button.remove.confirm"), i18n("server.delete"), - () -> Task.runAsync(() -> { - ServerHolder holder = server.holder; - if (holder == null) return; - - Task.runAsync(Schedulers.io(), () -> { - List servers = Server.getServers(holder.fromServersDatFilePath); - servers.remove(server); - Server.saveServerToDat(holder.fromServersDatFilePath, servers); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) - LOG.warning("Failed to save server data.", exception); - - refresh(); - }).start(); - + () -> Task.runAsync(Schedulers.io(), () -> { + List servers = holder.cachedServers; + servers.remove(holder.inDatPathSlot); + Server.saveToServersDat(servers, holder.fromServersDatFilePath); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) + LOG.warning("Failed to save server data.", exception); + + refresh(); }).start(), null ); } - public void copyToInstance(Server server) { - ServerHolder holder = server.holder; - if (holder == null) return; + public void copyToInstance(ServerHolder holder) { + addServer(holder.server); + } + + public void addServer(Server server) { if (gameInstance == null) return; Task.runAsync(Schedulers.io(), () -> { - List servers = Server.getServers(gameInstance.getServersDatFilePath()); + List servers = Server.loadFromServersDat(gameInstance.getServersDatFilePath()); servers.add(server); - Server.saveServerToDat(gameInstance.getServersDatFilePath(), servers); + Server.saveToServersDat(servers, gameInstance.getServersDatFilePath()); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); @@ -139,9 +134,9 @@ public void copyToInstance(Server server) { }).start(); } - public void generateLaunchScript(Server server) { + public void generateLaunchScript(ServerHolder holder) { if (gameInstance != null) { - Instances.generateLaunchScriptForQuickConnectServer(gameInstance, server.ip); + Instances.generateLaunchScriptForQuickConnectServer(gameInstance, holder.server.getIp()); } } @@ -161,12 +156,17 @@ private void updateServerList() { stream = stream.filter(holder -> holder.fromServersDatFilePath.equals(gameInstance.getServersDatFilePath())); } if (!showHide.get()) { - stream = stream.filter(holder -> !holder.server.hidden); + stream = stream.filter(holder -> !holder.server.isHidden()); } getItems().setAll(stream.toList()); } + private void addServer() { + runInFX(() -> Controllers.dialog(new AddServerPane(() -> { + }, this::addServer))); + } + private void refresh() { if (gameInstance == null) return; @@ -186,14 +186,15 @@ private void refresh() { ArrayList holders = new ArrayList<>(); pathListMap.put(instance.getServersDatFilePath(), holders); - List parsedServers = Server.getServers(instance.getServersDatFilePath()); - for (int index = 0; index < parsedServers.size(); index++) { - Server server = parsedServers.get(index); - ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), index, server); - server.holder = holder; - holder.holdInstances.add(instance); + if (Files.exists(instance.getServersDatFilePath())) { + List parsedServers = Server.loadFromServersDat(instance.getServersDatFilePath()); + for (int index = 0; index < parsedServers.size(); index++) { + Server server = parsedServers.get(index); + ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), parsedServers, index, IconedServer.pack(server)); + holder.holdInstances.add(instance); - holders.add(holder); + holders.add(holder); + } } } } @@ -262,7 +263,7 @@ public ServerListCell(ServerListPage page) { btnLaunch.setOnAction(event -> { ServerHolder holder = getItem(); if (holder != null) - page.launchAndEnterServer(holder.server); + page.launchAndEnterServer(holder); }); JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); @@ -307,12 +308,12 @@ protected void updateItem(ServerHolder holder, boolean empty) { content.setTitle(""); content.setSubtitle(""); } else { - imageView.setImage(holder.server.iconImage == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : holder.server.iconImage); - leftTooltip.setText(holder.server.ip); - content.setTitle(holder.server.name != null ? parseColorEscapes(holder.server.name) : ""); - content.setSubtitle(holder.server.ip); + imageView.setImage(holder.server.iconImage); + leftTooltip.setText(holder.server.getIp()); + content.setTitle(holder.server.getName() != null ? parseColorEscapes(holder.server.getName()) : ""); + content.setSubtitle(holder.server.getIp()); - if (holder.server.hidden) { + if (holder.server.isHidden()) { content.addTag(i18n("server.tag.hide")); } @@ -338,22 +339,22 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); - IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder.server), popup); + IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); popupMenu.getContent().addAll( new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_connect_server"), () -> - page.launchAndEnterServer(holder.server), popup + page.launchAndEnterServer(holder), popup ), new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> - page.generateLaunchScript(holder.server), popup + page.generateLaunchScript(holder), popup ), new MenuSeparator(), new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.server.ip"), () -> - page.copyServerIp(holder.server), popup + page.copyServerIp(holder), popup ), new MenuSeparator(), copyToInstanceMEnuItem, new IconedMenuItem(SVG.DELETE_FOREVER, i18n("server.delete"), () -> - page.delete(holder.server), popup + page.delete(holder), popup ) ); if (page.gameInstance != null) { @@ -365,46 +366,30 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos } } - public static final class ServerHolder { - final @NotNull List holdInstances = new ArrayList<>(); - final @NotNull Path fromServersDatFilePath; - final int inDatPathSlot; - final @NotNull Server server; + public static class IconedServer extends Server { + final Image iconImage; - public ServerHolder(@NotNull Path fromServersDatFilePath, int inDatPathSlot, @NotNull Server server) { - this.fromServersDatFilePath = fromServersDatFilePath; - this.inDatPathSlot = inDatPathSlot; - this.server = server; + public IconedServer(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { + super(acceptTextures, hidden, icon, ip, name); + + Image parsedImage = parseImage(icon); + iconImage = parsedImage == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : parsedImage; } - } - public static final class Server { - final boolean acceptTextures; - final boolean hidden; - final @Nullable String icon; - final @Nullable String ip; - final @Nullable String name; - - transient final Image iconImage; - transient ServerHolder holder; - - public Server( - boolean acceptTextures, - boolean hidden, - @Nullable String icon, - @Nullable String ip, - @Nullable String name - ) { - this.acceptTextures = acceptTextures; - this.hidden = hidden; - this.icon = icon; - this.ip = ip; - this.name = name; - - this.iconImage = parseImage(icon); + public static IconedServer pack(Server server) { + if (server instanceof IconedServer) { + return (IconedServer) server; + } + return new IconedServer( + server.isAcceptTextures(), + server.isHidden(), + server.getIcon(), + server.getIp(), + server.getName() + ); } - public static Image parseImage(String imageBase64String) { + private static Image parseImage(String imageBase64String) { if (imageBase64String == null || imageBase64String.isEmpty()) { return null; } @@ -416,84 +401,20 @@ public static Image parseImage(String imageBase64String) { return null; } } + } - public static List getServers(@NotNull Path serversDatFilePath) { - List set = new ArrayList<>(); - if (Files.exists(serversDatFilePath)) { - try { - CompoundTag tags = (CompoundTag) NBTCodec.of(MinecraftEdition.JAVA_EDITION).readTag(serversDatFilePath); - Tag serversTag = tags.get("servers"); - if (serversTag instanceof ListTag st) { - for (Tag tag : st) { - if (tag instanceof CompoundTag cTag) { - set.add(new Server( - cTag.get("acceptTextures") instanceof ByteTag bt && bt.getValue() != 0, - cTag.get("hidden") instanceof ByteTag bt && bt.getValue() != 0, - cTag.get("icon") instanceof StringTag stg ? stg.getValue() : null, - cTag.get("ip") instanceof StringTag stg ? stg.getValue() : null, - cTag.get("name") instanceof StringTag stg ? stg.getValue() : null - )); - } - } - } - } catch (IOException e) { - LOG.error("Failed to read servers.dat file.", e); - } - } - return set; - } - - public static void saveServerToDat(Path datFile, List saveServerData) { - ListTag tag = new ListTag<>(); - for (Server server : saveServerData) { - CompoundTag serverTag = new CompoundTag(); - server.writeToCompoundTag(serverTag); - tag.addTag(serverTag); - } - - CompoundTag root = new CompoundTag(); - root.addTag("servers", tag); - - try (var output = Files.newOutputStream(datFile)) { - NBTCodec.of(MinecraftEdition.JAVA_EDITION).writeTag(output, root); - } catch (IOException e) { - LOG.error("Failed to write servers.dat file.", e); - } - } - - public void writeToCompoundTag(CompoundTag tag) { - tag.addByte("acceptTextures", (byte) (acceptTextures ? 1 : 0)); - tag.addByte("hidden", (byte) (hidden ? 1 : 0)); - if (icon != null) tag.addString("icon", icon); - if (ip != null) tag.addString("ip", ip); - if (name != null) tag.addString("name", name); - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - Server that = (Server) o; - return Objects.equals(ip, that.ip) && - Objects.equals(name, that.name) && - Objects.equals(acceptTextures, that.acceptTextures) && - Objects.equals(hidden, that.hidden) && - Objects.equals(icon, that.icon); - } - - @Override - public int hashCode() { - return Objects.hash(ip, name); - } + public static final class ServerHolder { + final @NotNull List holdInstances = new ArrayList<>(); + final @NotNull Path fromServersDatFilePath; + final @NotNull List cachedServers; + final int inDatPathSlot; + final @NotNull IconedServer server; - @Override - public String toString() { - return "Server{" + - "acceptTextures=" + acceptTextures + - ", hidden=" + hidden + - ", icon='" + icon + '\'' + - ", ip='" + ip + '\'' + - ", name='" + name + '\'' + - '}'; + public ServerHolder(@NotNull Path fromServersDatFilePath, @NotNull List cachedServers, int inDatPathSlot, @NotNull IconedServer server) { + this.fromServersDatFilePath = fromServersDatFilePath; + this.inDatPathSlot = inDatPathSlot; + this.cachedServers = cachedServers; + this.server = server; } } @@ -520,7 +441,8 @@ protected List initializeToolbar(ServerListPage skinnable) { return Arrays.asList( chkShowAll, chkShowHide, - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh) + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), + createToolbarButton2(i18n("servers.manager.add"), SVG.ADD, skinnable::addServer) ); } 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 48f4b59c66a..7dce19395a0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1638,4 +1638,10 @@ servers.manage.delete=删除 server.delete=删除此服务器 server.tag.hide=隐藏 server.tag.hold.current=当前实例 -server.empty=没有服务器 \ No newline at end of file +server.empty=没有服务器 +servers.manager.add=添加服务器 +servers.manager.add.head=添加服务器 +servers.manager.add.server.name=服务器名字 +servers.manager.add.server.name.def=Minecraft 服务器 +servers.manager.add.server.ip=服务器IP +servers.manager.add.accept=添加 \ No newline at end of file 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 59cf8626e37..fa95cecc85b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1632,4 +1632,10 @@ servers.manage.delete=刪除 server.delete=刪除此伺服器 server.tag.hide=隱藏 server.tag.hold.current=目前實例 -server.empty=沒有伺服器 \ No newline at end of file +server.empty=沒有伺服器 +servers.manager.add=新增伺服器 +servers.manager.add.head=新增伺服器 +servers.manager.add.server.name=伺服器名稱 +servers.manager.add.server.name.def=Minecraft 伺服器 +servers.manager.add.server.ip=伺服器位址 +servers.manager.add.accept=新增 \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java new file mode 100644 index 00000000000..58b8dc75ed3 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -0,0 +1,133 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import org.glavo.nbt.tag.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import static org.jackhuang.hmcl.util.NBTUtils.readCompressed; +import static org.jackhuang.hmcl.util.NBTUtils.writeCompressed; + +public class Server { + + private final boolean acceptTextures; + private final boolean hidden; + private final @Nullable String icon; + private final @Nullable String ip; + private final @Nullable String name; + + public Server(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { + this.acceptTextures = acceptTextures; + this.hidden = hidden; + this.icon = icon; + this.ip = ip; + this.name = name; + } + + public static Server loadSingle(@NotNull CompoundTag tag) { + return new Server( + tag.get("acceptTextures") instanceof ByteTag bt && bt.getValue() != 0, + tag.get("hidden") instanceof ByteTag bt && bt.getValue() != 0, + tag.get("icon") instanceof StringTag stg ? stg.getValue() : null, + tag.get("ip") instanceof StringTag stg ? stg.getValue() : null, + tag.get("name") instanceof StringTag stg ? stg.getValue() : null + ); + } + + public static List loadFromServersDat(@NotNull Path file) throws IOException { + List servers = new ArrayList<>(); + + CompoundTag root = readCompressed(file); + Tag serversTag = root.get("servers"); + if (serversTag == null) + throw new IOException("servers tag not found"); + else if (!(serversTag instanceof ListTag)) + throw new IOException("servers tag is not a ListTag"); + + for (Tag serverEntryTag : ((ListTag) serversTag)) { + if (serverEntryTag instanceof CompoundTag serverEntryTagCompound) { + servers.add(loadSingle(serverEntryTagCompound)); + } else { + throw new IOException("server entry tag is not a CompoundTag"); + } + } + + return servers; + } + + public static void saveToServersDat(@NotNull List servers, @NotNull Path file) throws IOException { + ListTag tag = new ListTag<>(); + for (Server server : servers) { + CompoundTag serverTag = new CompoundTag(); + server.writeToCompoundTag(serverTag); + tag.addTag(serverTag); + } + + CompoundTag root = new CompoundTag(); + root.addTag("servers", tag); + + writeCompressed(root, file); + } + + public void writeToCompoundTag(CompoundTag tag) { + tag.addByte("acceptTextures", (byte) (acceptTextures ? 1 : 0)); + tag.addByte("hidden", (byte) (hidden ? 1 : 0)); + if (icon != null) tag.addString("icon", icon); + if (ip != null) tag.addString("ip", ip); + if (name != null) tag.addString("name", name); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Server server = (Server) o; + return acceptTextures == server.acceptTextures && hidden == server.hidden && Objects.equals(icon, server.icon) && Objects.equals(ip, server.ip) && Objects.equals(name, server.name); + } + + @Override + public int hashCode() { + return Objects.hash(acceptTextures, hidden, icon, ip, name); + } + + public boolean isAcceptTextures() { + return acceptTextures; + } + + public boolean isHidden() { + return hidden; + } + + public @Nullable String getIcon() { + return icon; + } + + public @Nullable String getIp() { + return ip; + } + + public @Nullable String getName() { + return name; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java index 7bd853f7591..2fa268b58eb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java @@ -22,6 +22,7 @@ import org.jetbrains.annotations.Nullable; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.Optional; @@ -34,6 +35,12 @@ public static CompoundTag readCompressed(Path file) throws IOException { return NBTCodec.of().readTag(file, TagType.COMPOUND); } + public static void writeCompressed(CompoundTag tag, Path file) throws IOException { + try (var output = Files.newOutputStream(file)) { + NBTCodec.of().writeTag(output, tag); + } + } + public static OptionalLong tryGetLong(Tag tag) { if (tag instanceof ValueTag valueTag) { Object val = valueTag.getValue(); From f347fe66c3271ab7d72428eb88cf7b11abff63af Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 23 Sep 2026 01:49:17 +0800 Subject: [PATCH 04/97] add server status check --- .../ui/instances/server/AddServerPane.java | 88 +++++-- .../ui/instances/server/ServerListPage.java | 3 +- .../assets/lang/I18N_zh_Hans.properties | 4 +- .../assets/lang/I18N_zh_Hant.properties | 4 +- .../jackhuang/hmcl/server/ServerStatus.java | 32 +++ .../hmcl/server/ServerStatusGetter.java | 219 ++++++++++++++++++ 6 files changed, 321 insertions(+), 29 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java index 056995b66a0..66f84bde756 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java @@ -25,10 +25,10 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; -import javafx.scene.layout.ColumnConstraints; -import javafx.scene.layout.GridPane; -import javafx.scene.layout.Priority; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; +import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; @@ -40,20 +40,33 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.setValidateWhileTextChanged; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class AddServerPane extends TransitionPane implements DialogAware { - private final Runnable failed; - private final Consumer success; + private final Consumer doAddServer; - private final GridPane body; - private final JFXTextField txtServerName; - private final JFXTextField txtServerIP; + private final GridPane body = new GridPane(); + private final JFXTextField txtServerName = new JFXTextField(); + private final JFXTextField txtServerIP = new JFXTextField(); - public AddServerPane(Runnable failed, Consumer success) { - this.failed = failed; - this.success = success; + private final JFXButton btnAccept = new JFXButton(); + private final JFXButton btnCancel = new JFXButton(); + private final SpinnerPane spinner = new SpinnerPane(); + private final Label lblErrorMessage = new Label(); + + public AddServerPane(Consumer doAddServer) { + this.doAddServer = doAddServer; getStyleClass().add("skin-pane"); + initPaneContents(); + + } + + private void initPaneContents() { + body.setDisable(false); + body.getChildren().clear(); + body.getColumnConstraints().clear(); + lblErrorMessage.setText(""); JFXDialogLayout rootLayout = new JFXDialogLayout(); getChildren().setAll(rootLayout); @@ -61,7 +74,6 @@ public AddServerPane(Runnable failed, Consumer succ VBox bodyVbox = new VBox(); bodyVbox.setSpacing(8); - body = new GridPane(); VBox.setMargin(body, new Insets(20, 0, 0, 0)); body.setVgap(22); @@ -82,7 +94,6 @@ public AddServerPane(Runnable failed, Consumer succ GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 0); - txtServerName = new JFXTextField(); txtServerName.setPromptText(i18n("servers.manager.add.server.name.def")); body.add(txtServerName, 1, 0); } @@ -94,7 +105,6 @@ public AddServerPane(Runnable failed, Consumer succ GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); - txtServerIP = new JFXTextField(); txtServerIP.setValidators(new RequiredValidator()); setValidateWhileTextChanged(txtServerIP, true); body.add(txtServerIP, 1, 1); @@ -103,7 +113,7 @@ public AddServerPane(Runnable failed, Consumer succ bodyVbox.getChildren().add(body); rootLayout.setBody(bodyVbox); - JFXButton btnAccept = new JFXButton(i18n("servers.manager.add.accept")); + btnAccept.setText(i18n("servers.manager.add.accept")); btnAccept.getStyleClass().add("dialog-accept"); btnAccept.setOnAction(e -> onAdd()); btnAccept.disableProperty().bind(new BooleanBinding() { @@ -117,36 +127,64 @@ protected boolean computeValue() { } }); - JFXButton btnCancel = new JFXButton(i18n("button.cancel")); + lblErrorMessage.setWrapText(true); + lblErrorMessage.setMaxWidth(400); + + btnCancel.setText(i18n("button.cancel")); btnCancel.getStyleClass().add("dialog-cancel"); btnCancel.setOnAction(e -> onCancel()); onEscPressed(this, btnCancel::fire); - SpinnerPane spinner = new SpinnerPane(); spinner.getStyleClass().add("small-spinner-pane"); spinner.setContent(btnAccept); - rootLayout.setActions(btnAccept, btnCancel); + HBox actions = new HBox(spinner, btnCancel); + actions.setAlignment(Pos.CENTER_RIGHT); + + rootLayout.setActions(lblErrorMessage, actions); } private void onCancel() { fireEvent(new DialogCloseEvent()); - failed.run(); } private void onAdd() { - String serverName = txtServerName.getText(); + spinner.showSpinner(); + String serverName; String serverIP = txtServerIP.getText(); if (serverIP == null) return; - if (serverName == null || serverName.isEmpty()) { + if (txtServerName.getText() == null || txtServerName.getText().isEmpty()) { serverName = i18n("servers.manager.add.server.name.def"); + } else { + serverName = txtServerName.getText(); } + body.setDisable(true); - fireEvent(new DialogCloseEvent()); - // todo online check - success.accept(new ServerListPage.IconedServer(true, false, null, serverName, serverIP)); + Task.supplyAsync(Schedulers.io(), () -> + ServerStatusGetter.getStatus(serverIP) + ).whenComplete(Schedulers.javafx(), (status, exception) -> { + if (exception != null) + LOG.warning("Failed to fetch server status.", exception); + + if (status == null) { + spinner.hideSpinner(); + btnAccept.setText(i18n("servers.manager.add.still")); + btnAccept.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + doAddServer.accept(new ServerListPage.IconedServer(false, false, null, serverIP, serverName)); + }); + btnCancel.setOnAction(e -> { + initPaneContents(); + }); + lblErrorMessage.setText(i18n("servers.manager.add.error.status")); + } else { +// initStatusContents(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); + fireEvent(new DialogCloseEvent()); + doAddServer.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); + } + }).start(); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 6705e6cf94e..f1d9dcd4146 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -163,8 +163,7 @@ private void updateServerList() { } private void addServer() { - runInFX(() -> Controllers.dialog(new AddServerPane(() -> { - }, this::addServer))); + runInFX(() -> Controllers.dialog(new AddServerPane(this::addServer))); } private void refresh() { 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 7dce19395a0..f75c5e93ce7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1644,4 +1644,6 @@ servers.manager.add.head=添加服务器 servers.manager.add.server.name=服务器名字 servers.manager.add.server.name.def=Minecraft 服务器 servers.manager.add.server.ip=服务器IP -servers.manager.add.accept=添加 \ No newline at end of file +servers.manager.add.accept=添加 +servers.manager.add.still=依然添加 +servers.manager.add.error.status=无法获取目标服务器状态 \ No newline at end of file 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 fa95cecc85b..a82a4b49aec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1638,4 +1638,6 @@ servers.manager.add.head=新增伺服器 servers.manager.add.server.name=伺服器名稱 servers.manager.add.server.name.def=Minecraft 伺服器 servers.manager.add.server.ip=伺服器位址 -servers.manager.add.accept=新增 \ No newline at end of file +servers.manager.add.accept=新增 +servers.manager.add.still=依然新增 +servers.manager.add.error.status=無法獲取目標伺服器狀態 \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java new file mode 100644 index 00000000000..c9bff044ec5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -0,0 +1,32 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +public record ServerStatus( + // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Pong_Response + long networkLatency, + + // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response + int protocol, + String protocolName, + + int playerMax, + int playerOnline, + String favicon +) { +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java new file mode 100644 index 00000000000..f1a5d92d0d0 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java @@ -0,0 +1,219 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.*; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +public final class ServerStatusGetter { + private ServerStatusGetter() { + + } + + private static InetSocketAddress parseAddress(String ipStr) throws IOException { + if (ipStr == null || ipStr.isEmpty()) throw new IOException("server ip is empty."); + + String host = ipStr; + int port = 25565; + + int lastIndexOf = ipStr.lastIndexOf(":"); + if (lastIndexOf != -1) { + String portArea = ipStr.substring(lastIndexOf + 1); + try { + int parsedPort = Integer.parseInt(portArea); + if (parsedPort > 0 && parsedPort <= 65535) { + host = ipStr.substring(0, lastIndexOf); + port = parsedPort; + } + } catch (Exception ignore) { + } + } + return new InetSocketAddress(host, port); + } + + public static ServerStatus getStatus(String serverIp) throws IOException { + return ServerStatusGetter.getStatus(parseAddress(serverIp)); + } + + public static ServerStatus getStatus(InetSocketAddress address) throws IOException { + try (Socket socket = new Socket()) { + socket.connect(address); + socket.setSoTimeout(10_000); + + try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + DataInputStream in = new DataInputStream(socket.getInputStream())) { + + sendHandshakeStatusPacket(out, address.getHostName(), address.getPort()); + sendStatusRequestPacket(out); + var status = readStatusResponsePacket(in); + + long sendTime = System.currentTimeMillis(); + sendPingRequestPacket(out); + var pong = readPongResponsePacket(in); + long networkLatency = System.currentTimeMillis() - sendTime; + + JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); + + JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); + int protocol = versionJsonObject.get("protocol").getAsInt(); + String protocolName = versionJsonObject.get("name").getAsString(); + + JsonObject playersJsonObject = rootStatus.getAsJsonObject("players"); + int playerMax = playersJsonObject.get("max").getAsInt(); + int playerOnline = playersJsonObject.get("online").getAsInt(); + + + // todo check + String favicon = rootStatus.get("favicon").getAsString().substring("data:image/png;base64,".length()); + + return new ServerStatus( + networkLatency, + protocol, + protocolName, + playerMax, + playerOnline, + favicon + ); + } + } + } + + private static long readPongResponsePacket(DataInputStream in) throws IOException { + byte[] packetData = new byte[readVarInt(in)]; + in.readFully(packetData); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(packetData); + DataInputStream packetIn = new DataInputStream(bais)) { + int packetId = readVarInt(packetIn); + if (packetId != 0x01) + throw new IOException("Invalid packet id " + packetId + ", expected 0x01(Pong Response)."); + return readLong(packetIn); + } + } + + private static String readStatusResponsePacket(DataInputStream in) throws IOException { + byte[] packetData = new byte[readVarInt(in)]; + in.readFully(packetData); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(packetData); + DataInputStream packetIn = new DataInputStream(bais)) { + int packetId = readVarInt(packetIn); + if (packetId != 0x00) + throw new IOException("Invalid packet id " + packetId + ", expected 0x00(Status Response)."); + byte[] jsonBytes = new byte[readVarInt(packetIn)]; + packetIn.readFully(jsonBytes); + return new String(jsonBytes, StandardCharsets.UTF_8); + } + } + + private static void sendStatusRequestPacket(DataOutputStream sendTarget) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + output.write(0x00); // Packet ID + + writeVarInt(sendTarget, output.size()); // Packet Size + sendTarget.write(output.toByteArray()); // Packet Contents + } + } + + private static void sendHandshakeStatusPacket(DataOutputStream sendTarget, String address, int port) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + output.write(0x00); // Packet ID + writeVarInt(output, -1); // Protocol Version + + byte[] addressBytes = address.getBytes(StandardCharsets.UTF_8); + writeVarInt(output, addressBytes.length); + output.write(addressBytes); + + // Server Port + output.write((port >>> 8) & 0xFF); + output.write(port & 0xFF); + + // Next State Status Request + writeVarInt(output, 1); + + writeVarInt(sendTarget, output.size()); // Packet Size + sendTarget.write(output.toByteArray()); // Packet Contents + } + } + + private static void sendPingRequestPacket(DataOutputStream sendTarget) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + output.write(0x01); // packet id + long time = System.currentTimeMillis(); + + writeLong(output, time); // time + + writeVarInt(sendTarget, output.size()); + sendTarget.write(output.toByteArray()); + } + } + + private static void writeVarInt(OutputStream out, int value) throws IOException { + while ((value & ~0x7F) != 0) { + out.write((value & 0x7F) | 0x80); + + value >>>= 7; + } + + out.write(value); + } + + private static int readVarInt(InputStream in) throws IOException { + int value = 0; + int position = 0; + int currentByte; + + while (position < 32) { + currentByte = in.read(); + if (currentByte == -1) { + throw new EOFException(""); + } + + value |= (currentByte & 0x7F) << position; + + if ((currentByte & 0x80) == 0) + return value; + position += 7; + } + + throw new IOException("VarInt too big"); + } + + private static void writeLong(OutputStream out, long value) throws IOException { + for (int i = 7; i >= 0; i--) { + out.write((int) ((value >>> (i * 8)) & 0xFF)); + } + } + + private static long readLong(InputStream in) throws IOException { + long value = 0; + for (int i = 0; i < 8; i++) { + int b = in.read(); + if (b == -1) { + throw new EOFException(""); + } + value = (value << 8) | (b & 0xFF); + } + return value; + } +} From 277ea3cfa182789390f9077cc9f601d742d28f5d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 23 Sep 2026 18:39:31 +0800 Subject: [PATCH 05/97] srv support --- .../jackhuang/hmcl/server/ServerAddress.java | 59 +++++++++ .../hmcl/server/ServerDnsSrvRedirector.java | 78 ++++++++++++ .../jackhuang/hmcl/server/ServerStatus.java | 4 +- .../hmcl/server/ServerStatusGetter.java | 117 +++++++++--------- 4 files changed, 200 insertions(+), 58 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java new file mode 100644 index 00000000000..4c559962060 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java @@ -0,0 +1,59 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public record ServerAddress( + String host, + int port +) { + public static int parsePort(final String str) { + try { + return Integer.parseInt(str.trim()); + } catch (Exception var2) { + return 25565; + } + } + + public static ServerAddress parseAddress(String ipStr) throws IOException { + if (ipStr == null || ipStr.isEmpty()) throw new IOException("server ip is empty."); + + String host = ipStr; + int port = 25565; + + int lastIndexOf = ipStr.lastIndexOf(":"); + if (lastIndexOf != -1) { + String portArea = ipStr.substring(lastIndexOf + 1); + try { + int parsedPort = Integer.parseInt(portArea); + if (parsedPort > 0 && parsedPort <= 65535) { + host = ipStr.substring(0, lastIndexOf); + port = parsedPort; + } + } catch (Exception ignore) { + } + } + return new ServerAddress(host, port); + } + + public InetSocketAddress toInetSocketAddress() { + return new InetSocketAddress(host, port); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java new file mode 100644 index 00000000000..70584a10613 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java @@ -0,0 +1,78 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import org.jetbrains.annotations.NotNull; + +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import java.io.IOException; +import java.util.Hashtable; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class ServerDnsSrvRedirector { + private static volatile DnsSrvRedirector redirector; + + private ServerDnsSrvRedirector() { + + } + + static @NotNull ServerAddress lookup(ServerAddress original) throws IOException { + if (redirector == null) { + synchronized (ServerDnsSrvRedirector.class) { + if (redirector == null) { + + try { + Class.forName("com.sun.jndi.dns.DnsContextFactory"); + Hashtable env = new Hashtable<>(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns:"); + env.put("com.sun.jndi.dns.timeout.retries", "1"); + DirContext context = new InitialDirContext(env); + + redirector = (address) -> { + if (address.port() != 25565) return address; + try { + Attributes attributes = context.getAttributes("_minecraft._tcp." + address.host(), new String[]{"SRV"}); + Attribute srvAttribute = attributes.get("srv"); + if (srvAttribute != null) { + String[] arguments = srvAttribute.get().toString().split(" ", 4); + return new ServerAddress(arguments[3], ServerAddress.parsePort(arguments[2])); + } + } catch (Throwable ignored) { + } + return address; + }; + } catch (Throwable e) { + LOG.error("Failed to initialize SRV redirect resolved, some servers might not work", e); + redirector = (address) -> address; + } + } + } + } + return redirector.lookup(original); + } + + @FunctionalInterface + private interface DnsSrvRedirector { + @NotNull ServerAddress lookup(@NotNull ServerAddress original) throws IOException; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index c9bff044ec5..5aa8e58cb4a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.server; +import org.jetbrains.annotations.Nullable; + public record ServerStatus( // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Pong_Response long networkLatency, @@ -27,6 +29,6 @@ public record ServerStatus( int playerMax, int playerOnline, - String favicon + @Nullable String favicon ) { } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java index f1a5d92d0d0..fec39c64aa7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java @@ -21,8 +21,8 @@ import com.google.gson.JsonParser; import java.io.*; -import java.net.InetSocketAddress; import java.net.Socket; +import java.net.StandardSocketOptions; import java.nio.charset.StandardCharsets; public final class ServerStatusGetter { @@ -30,40 +30,21 @@ private ServerStatusGetter() { } - private static InetSocketAddress parseAddress(String ipStr) throws IOException { - if (ipStr == null || ipStr.isEmpty()) throw new IOException("server ip is empty."); - - String host = ipStr; - int port = 25565; - - int lastIndexOf = ipStr.lastIndexOf(":"); - if (lastIndexOf != -1) { - String portArea = ipStr.substring(lastIndexOf + 1); - try { - int parsedPort = Integer.parseInt(portArea); - if (parsedPort > 0 && parsedPort <= 65535) { - host = ipStr.substring(0, lastIndexOf); - port = parsedPort; - } - } catch (Exception ignore) { - } - } - return new InetSocketAddress(host, port); - } - public static ServerStatus getStatus(String serverIp) throws IOException { - return ServerStatusGetter.getStatus(parseAddress(serverIp)); + return ServerStatusGetter.getStatus(ServerAddress.parseAddress(serverIp)); } - public static ServerStatus getStatus(InetSocketAddress address) throws IOException { + private static ServerStatus getStatus(ServerAddress address) throws IOException { + ServerAddress resolvedAddress = ServerDnsSrvRedirector.lookup(address); + try (Socket socket = new Socket()) { - socket.connect(address); - socket.setSoTimeout(10_000); + socket.setOption(StandardSocketOptions.TCP_NODELAY, true); + socket.connect(resolvedAddress.toInetSocketAddress(), 7_000); try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); DataInputStream in = new DataInputStream(socket.getInputStream())) { - sendHandshakeStatusPacket(out, address.getHostName(), address.getPort()); + sendHandshakeStatusPacket(out, address.host(), address.port()); sendStatusRequestPacket(out); var status = readStatusResponsePacket(in); @@ -82,9 +63,13 @@ public static ServerStatus getStatus(InetSocketAddress address) throws IOExcepti int playerMax = playersJsonObject.get("max").getAsInt(); int playerOnline = playersJsonObject.get("online").getAsInt(); - - // todo check - String favicon = rootStatus.get("favicon").getAsString().substring("data:image/png;base64,".length()); + String favicon = null; + if (rootStatus.get("favicon") != null) { + String fetchedFavicon = rootStatus.get("favicon").getAsString(); + if (fetchedFavicon.startsWith("data:image/png;base64,")) { + favicon = fetchedFavicon.substring("data:image/png;base64,".length()); + } + } return new ServerStatus( networkLatency, @@ -120,52 +105,53 @@ private static String readStatusResponsePacket(DataInputStream in) throws IOExce int packetId = readVarInt(packetIn); if (packetId != 0x00) throw new IOException("Invalid packet id " + packetId + ", expected 0x00(Status Response)."); - byte[] jsonBytes = new byte[readVarInt(packetIn)]; - packetIn.readFully(jsonBytes); - return new String(jsonBytes, StandardCharsets.UTF_8); + + return readVarString(packetIn); // Status Json } } private static void sendStatusRequestPacket(DataOutputStream sendTarget) throws IOException { - try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { - output.write(0x00); // Packet ID + sendPacket(sendTarget, out -> { + out.writeByte(0x00); // Packet ID + }); + } - writeVarInt(sendTarget, output.size()); // Packet Size - sendTarget.write(output.toByteArray()); // Packet Contents + private static void sendPacket(DataOutputStream target, PacketWriter packetWriter) throws IOException { + try (ByteArrayOutputStream packetOut = new ByteArrayOutputStream(); + DataOutputStream dataPacketOut = new DataOutputStream(packetOut)) { + packetWriter.write(dataPacketOut); + + writeVarInt(target, packetOut.size()); // Packet Size + target.write(packetOut.toByteArray()); // Packet Contents } } private static void sendHandshakeStatusPacket(DataOutputStream sendTarget, String address, int port) throws IOException { - try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { - output.write(0x00); // Packet ID - writeVarInt(output, -1); // Protocol Version + sendPacket(sendTarget, out -> { + // Packet ID + out.write(0x00); + + // Protocol Version + writeVarInt(out, 777); - byte[] addressBytes = address.getBytes(StandardCharsets.UTF_8); - writeVarInt(output, addressBytes.length); - output.write(addressBytes); + // Server Host + writeVarString(out, address); // Server Port - output.write((port >>> 8) & 0xFF); - output.write(port & 0xFF); + out.writeShort(port); // Next State Status Request - writeVarInt(output, 1); - - writeVarInt(sendTarget, output.size()); // Packet Size - sendTarget.write(output.toByteArray()); // Packet Contents - } + writeVarInt(out, 1); + }); } private static void sendPingRequestPacket(DataOutputStream sendTarget) throws IOException { - try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { - output.write(0x01); // packet id + sendPacket(sendTarget, out -> { + out.write(0x01); // packet id long time = System.currentTimeMillis(); - writeLong(output, time); // time - - writeVarInt(sendTarget, output.size()); - sendTarget.write(output.toByteArray()); - } + writeLong(out, time); // time + }); } private static void writeVarInt(OutputStream out, int value) throws IOException { @@ -178,6 +164,12 @@ private static void writeVarInt(OutputStream out, int value) throws IOException out.write(value); } + private static String readVarString(DataInputStream in) throws IOException { + byte[] bytes = new byte[readVarInt(in)]; + in.readFully(bytes); + return new String(bytes, StandardCharsets.UTF_8); + } + private static int readVarInt(InputStream in) throws IOException { int value = 0; int position = 0; @@ -205,6 +197,12 @@ private static void writeLong(OutputStream out, long value) throws IOException { } } + private static void writeVarString(OutputStream out, String string) throws IOException { + byte[] bytes = string.getBytes(StandardCharsets.UTF_8); + writeVarInt(out, bytes.length); + out.write(bytes); + } + private static long readLong(InputStream in) throws IOException { long value = 0; for (int i = 0; i < 8; i++) { @@ -216,4 +214,9 @@ private static long readLong(InputStream in) throws IOException { } return value; } + + @FunctionalInterface + private interface PacketWriter { + void write(DataOutputStream out) throws IOException; + } } From d6f1fd18053ab30da41fed52ee9bced1313d9760 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 23 Sep 2026 23:01:35 +0800 Subject: [PATCH 06/97] =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/AddServerPane.java | 10 +- .../ui/instances/server/ServerListPage.java | 28 ++- .../ui/instances/server/ServerStatusPane.java | 169 ++++++++++++++++++ HMCL/src/main/resources/assets/css/root.css | 10 ++ .../resources/assets/lang/I18N.properties | 16 +- .../resources/assets/lang/I18N_ar.properties | 16 +- .../resources/assets/lang/I18N_de.properties | 16 +- .../resources/assets/lang/I18N_es.properties | 16 +- .../resources/assets/lang/I18N_ja.properties | 16 +- .../resources/assets/lang/I18N_ru.properties | 16 +- .../resources/assets/lang/I18N_uk.properties | 16 +- .../assets/lang/I18N_zh_Hans.properties | 15 +- .../assets/lang/I18N_zh_Hant.properties | 12 +- 13 files changed, 326 insertions(+), 30 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java index 66f84bde756..c0f64f49f41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java @@ -59,7 +59,6 @@ public AddServerPane(Consumer doAddServer) { getStyleClass().add("skin-pane"); initPaneContents(); - } private void initPaneContents() { @@ -90,18 +89,18 @@ private void initPaneContents() { // server name { Label label = new Label(); - label.setText(i18n("servers.manager.add.server.name")); + label.setText(i18n("servers.manager.server.name")); GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 0); - txtServerName.setPromptText(i18n("servers.manager.add.server.name.def")); + txtServerName.setPromptText(i18n("servers.manager.server.name.def")); body.add(txtServerName, 1, 0); } // server ip { Label label = new Label(); - label.setText(i18n("servers.manager.add.server.ip")); + label.setText(i18n("servers.manager.server.ip")); GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); @@ -154,7 +153,7 @@ private void onAdd() { String serverIP = txtServerIP.getText(); if (serverIP == null) return; if (txtServerName.getText() == null || txtServerName.getText().isEmpty()) { - serverName = i18n("servers.manager.add.server.name.def"); + serverName = i18n("servers.manager.server.name.def"); } else { serverName = txtServerName.getText(); } @@ -180,7 +179,6 @@ private void onAdd() { }); lblErrorMessage.setText(i18n("servers.manager.add.error.status")); } else { -// initStatusContents(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); fireEvent(new DialogCloseEvent()); doAddServer.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index f1d9dcd4146..db7a185a8ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -92,6 +92,12 @@ public void launchAndEnterServer(ServerHolder holder) { } } + public void showServerStatus(ServerHolder holder) { + if (gameInstance != null) { + runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.server))); + } + } + public void copyServerIp(ServerHolder holder) { FXUtils.copyText(holder.server.getIp(), i18n("servers.manage.copy.server.ip.ok.toast")); } @@ -328,6 +334,23 @@ protected void updateItem(ServerHolder holder, boolean empty) { .forEach(content::addTag); } + // add status tag. +// Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(holder.server.getIp())) +// .whenComplete(Schedulers.javafx(), (status, throwable) -> { +// if (throwable != null) { +// LOG.error("Failed to get server status.", throwable); +// } +// +// if (status != null) { +// if (status.favicon() != null) { +// Image latestImage = IconedServer.parseImage(status.favicon()); +// if (latestImage != null) { +// imageView.setImage(latestImage); +// } +// } +// } +// }).start(); + setGraphic(graphic); } } @@ -340,6 +363,9 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); popupMenu.getContent().addAll( + new IconedMenuItem(SVG.CHAT, i18n("servers.manager.status"), () -> + page.showServerStatus(holder), popup + ), new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_connect_server"), () -> page.launchAndEnterServer(holder), popup ), @@ -388,7 +414,7 @@ public static IconedServer pack(Server server) { ); } - private static Image parseImage(String imageBase64String) { + public static @Nullable Image parseImage(String imageBase64String) { if (imageBase64String == null || imageBase64String.isEmpty()) { return null; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java new file mode 100644 index 00000000000..4c60f24106e --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -0,0 +1,169 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances.server; + +import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXDialogLayout; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.server.ServerStatus; +import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.animation.TransitionPane; +import org.jackhuang.hmcl.ui.construct.DialogAware; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.ImageContainer; +import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jetbrains.annotations.Nullable; + +import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public class ServerStatusPane extends TransitionPane implements DialogAware { + private final ServerListPage.IconedServer iconedServer; + private final SpinnerPane refreshSpinner = new SpinnerPane(); + private final JFXDialogLayout rootLayout = new JFXDialogLayout(); + private final Label lblErrorMessage = new Label(); + + private @Nullable ServerStatus status; + + public ServerStatusPane(ServerListPage.IconedServer iconedServer) { + this.iconedServer = iconedServer; + + getStyleClass().add("skin-pane"); + getChildren().setAll(rootLayout); + rootLayout.setHeading(new Label(i18n("servers.manager.status.head"))); + + { + JFXButton refreshBtn = new JFXButton(i18n("button.refresh")); + refreshBtn.getStyleClass().add("dialog-refresh"); + refreshBtn.setOnAction(e -> refresh()); + refreshSpinner.getStyleClass().add("small-spinner-pane"); + refreshSpinner.setContent(refreshBtn); + } + + JFXButton backBtn = new JFXButton(i18n("button.back")); + backBtn.getStyleClass().add("dialog-back"); + backBtn.setOnAction(e -> onClose()); + onEscPressed(this, backBtn::fire); + + HBox actions = new HBox(refreshSpinner, backBtn); + actions.setAlignment(Pos.CENTER_RIGHT); + + replaceBody(); + + lblErrorMessage.setWrapText(true); + lblErrorMessage.setMaxWidth(400); + rootLayout.getActions().addAll(lblErrorMessage, actions); + + + refresh(); + } + + public void replaceBody() { + HBox contentBox = new HBox(); + setMargin(contentBox, new Insets(20, 0, 0, 0)); + + Image serverIcon = iconedServer.iconImage; + if (status != null) { + String latestFavicon = status.favicon(); + if (latestFavicon != null) { + Image latestFaviconImage = ServerListPage.IconedServer.parseImage(latestFavicon); + if (latestFaviconImage != null) { + serverIcon = latestFaviconImage; + } + } + } + + int iconScale = 4; + ImageContainer imageView = new ImageContainer(32); + imageView.setImage(serverIcon); + + imageView.setScaleX(iconScale); + imageView.setScaleY(iconScale); + + StackPane canvasPane = new StackPane(imageView); + canvasPane.setPrefWidth(iconScale * 32); + canvasPane.setPrefHeight(iconScale * 32); + contentBox.getChildren().add(canvasPane); + + GridPane textPane = new GridPane(); + textPane.setAlignment(Pos.TOP_LEFT); + + textPane.setHgap(10); + textPane.setVgap(10); + + contentBox.getChildren().add(textPane); + HBox.setMargin(textPane, new Insets(15, 0, 0, 20)); + + int rows = 0; + textPane.add(new Label(i18n("servers.manager.server.name") + ":"), 0, rows); + textPane.add(new Label(iconedServer.getName()), 1, rows); + + rows++; + textPane.add(new Label(i18n("servers.manager.server.ip") + ":"), 0, rows); + textPane.add(new Label(iconedServer.getIp()), 1, rows); + + ServerStatus cachedServerStatus = status; + if (cachedServerStatus != null) { + + rows++; + textPane.add(new Label(i18n("servers.manager.server.latency") + ":"), 0, rows); + textPane.add(new Label(String.format("%,d", cachedServerStatus.networkLatency())), 1, rows); + + rows++; + textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); + textPane.add(new Label(cachedServerStatus.playerOnline() + "/" + cachedServerStatus.playerMax()), 1, rows); + } + + rootLayout.setBody(contentBox); + } + + private void refresh() { + lblErrorMessage.setText(""); + refreshSpinner.showSpinner(); + Task.supplyAsync(Schedulers.io(), () -> + ServerStatusGetter.getStatus(iconedServer.getIp()) + ).whenComplete(Schedulers.javafx(), (status, throwable) -> { + refreshSpinner.hideSpinner(); + + if (status == null) { + lblErrorMessage.setText(i18n("servers.manager.error.status")); + } + + this.status = status; + + if (throwable != null) { + LOG.error("Error while getting server status.", throwable); + } + + replaceBody(); + }).start(); + } + + private void onClose() { + fireEvent(new DialogCloseEvent()); + } +} diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 099a1e624c2..5bf9bc8a98a 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -577,6 +577,16 @@ -fx-text-fill: -monet-on-surface; } +.dialog-refresh { + -fx-text-fill: -monet-primary; + -fx-padding: 0.7em 0.8em; +} + +.dialog-back { + -fx-text-fill: -monet-on-surface-variant; + -fx-padding: 0.7em 0.8em; +} + /******************************************************************************* * * * Microsoft Login Dialog * diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b0a66e54eb7..973f1a6bf02 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -204,7 +204,7 @@ archive.version=Version assets.download=Downloading Assets assets.download_all=Validating Assets Integrity assets.index.malformed=Index files of downloaded assets are corrupted. You can resolve this problem by clicking "Manage → Update Game Assets" on the "Edit Instance" page. - +button.back=back button.cancel=Cancel button.change_source=Change Download Source button.clear=Clear @@ -1832,4 +1832,16 @@ servers.manage.delete=Delete server.delete=Delete this server server.tag.hide=Hidden server.tag.hold.current=Current -server.empty=No Server \ No newline at end of file +server.empty=No Server +servers.manager.add=Add Server +servers.manager.add.head=Add Server +servers.manager.server.name=Server Name +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=Server IP address +servers.manager.server.latency=Current latency +servers.manager.server.players=Online +servers.manager.add.accept=Add +servers.manager.add.still=Still Add +servers.manager.error.status=Unable to fetch the current server status +servers.manager.status=Server Status +servers.manager.status.head=Server Status \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 7eca8246246..ad199e0df31 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -205,7 +205,7 @@ archive.version=الإصدار assets.download=جارٍ تنزيل الأصول assets.download_all=جارٍ التحقق من سلامة الأصول assets.index.malformed=ملفات فهرس الأصول المنزّلة تالفة. يمكنك حل هذه المشكلة بالنقر على "إدارة ← تحديث أصول اللعبة" في صفحة "تعديل النسخة". - +button.back=العودة button.cancel=إلغاء button.change_source=تغيير مصدر التنزيل button.clear=مسح @@ -1829,4 +1829,16 @@ servers.manage.delete=حذف server.delete=حذف هذا الخادم server.tag.hide=مخفي server.tag.hold.current=المحمل -# server.empty= \ No newline at end of file +# server.empty= +servers.manager.add=إضافة الخادم +servers.manager.add.head=إضافة الخادم +servers.manager.server.name=اسم الخادم +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=عنوان IP الخادم +servers.manager.server.latency=التأخير الحالي +servers.manager.server.players=على الانترنت +servers.manager.add.accept=إضافة +servers.manager.add.still=لا تزال إضافة +servers.manager.error.status=غير قادر على الحصول على حالة الخادم الحالية +servers.manager.status=حالة الخادم +servers.manager.status.head=حالة الخادم \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 2d11abcef3f..0141aef7fc3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -203,7 +203,7 @@ archive.version=Version assets.download=Ressourcen werden heruntergeladen assets.download_all=Integrität der Ressourcen wird überprüft assets.index.malformed=Indexdateien der heruntergeladenen Ressourcen sind beschädigt. Sie können dieses Problem beheben, indem Sie auf der Seite "Instanz bearbeiten" auf "Verwalten → Spielressourcen aktualisieren" klicken. - +button.back=zurück button.cancel=Abbrechen button.change_source=Download-Quelle ändern button.clear=Leeren @@ -1831,4 +1831,16 @@ servers.manage.delete=Löschen server.delete=Diesen Server löschen server.tag.hide=Versteckt server.tag.hold.current=Aktuell -server.empty=Kein Server \ No newline at end of file +server.empty=Kein Server +servers.manager.add=Server hinzufügen +servers.manager.add.head=Server hinzufügen +servers.manager.server.name=Name des Servers +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=IP-Adresse des Servers +servers.manager.server.latency=Aktuelle Latenz +servers.manager.server.players=Online +servers.manager.add.accept=Hinzufügen +servers.manager.add.still=Noch Hinzufügen +servers.manager.error.status=Der aktuelle Serverstatus kann nicht abgerufen werden +servers.manager.status=Serverstatus +servers.manager.status.head=Serverstatus \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index b9a2604eb69..d8d27a9bfe6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -203,7 +203,7 @@ archive.version=Versión assets.download=Descargando assets assets.download_all=Verificando la integridad de los archivos assets.index.malformed=Los archivos de índice de los activos descargados estaban dañados. Puede intentar utilizar «Actualizar activos del juego» en la configuración de su instancia del juego para solucionar este problema. - +button.back=tagasi button.cancel=Cancelar button.change_source=Cambiar fuente de descarga button.clear=Limpiar @@ -1815,4 +1815,16 @@ servers.manage.delete=Eliminar server.delete=Eliminar este servidor server.tag.hide=Oculto server.tag.hold.current=PoseídoActual -# server.empty= \ No newline at end of file +# server.empty= +servers.manager.add=Serveri lisamine +servers.manager.add.head=Serveri lisamine +servers.manager.server.name=Serveri nimi +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=Serveri IP-aadress +servers.manager.server.latency=Praegune viivitus +servers.manager.server.players=Internetis +servers.manager.add.accept=Lisa +servers.manager.add.still=Lisa ikkagi +servers.manager.error.status=Serveri praeguse seisundi leidmine nurjus +servers.manager.status=Serveri seisund +servers.manager.status.head=Serveri seisund \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 5ac7894b5c1..e0d3396ad2f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -192,7 +192,7 @@ archive.version=バージョン assets.download=アセットのダウンロード assets.download_all=アセットの整合性チェック assets.index.malformed=アセットインデックスの形式が正しくありません。バージョン設定の[ゲームアセットファイルの更新]で再試行できます。 - +button.back=<戻る button.cancel=キャンセル button.change_source=ダウンロードソースの変更 button.clear=クリア @@ -1617,4 +1617,16 @@ servers.manage.delete=削除 server.delete=このサーバーを削除 server.tag.hide=隠しの server.tag.hold.current=保有中 -# server.empty= \ No newline at end of file +# server.empty= +servers.manager.add=サーバーの追加 +servers.manager.add.head=サーバーの追加 +servers.manager.server.name=サーバ名 +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=サーバipアドレス +servers.manager.server.latency=現在の遅延 +servers.manager.server.players=オンライン +servers.manager.add.accept=追加 +servers.manager.add.still=追加 +servers.manager.error.status=現在のサーバステータスを取得できません +servers.manager.status=サーバの状態 +servers.manager.status.head=サーバの状態 \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index a3554ea7a14..8a16a9da4aa 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -199,7 +199,7 @@ archive.version=Версия assets.download=Скачивание ассетов assets.download_all=Проверка целостности ассетов assets.index.malformed=Индексные файлы скачанных ассетов повреждены. Чтобы исправить это, нажмите «Инструменты → Обновить ассеты игры» на странице «Изменить сборку». - +button.back=назад button.cancel=Отмена button.change_source=Изменить источник скачивания button.clear=Очистить @@ -1749,4 +1749,16 @@ servers.manage.delete=Удалить server.delete=Удалить этот сервер server.tag.hide=Скрытое server.tag.hold.current=ВладёныйТекущий -server.empty=Нет Серверы \ No newline at end of file +server.empty=Нет Серверы +servers.manager.add=Добавить сервер +servers.manager.add.head=Добавить сервер +servers.manager.server.name=Имя сервера +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=IP-адрес сервера +servers.manager.server.latency=Текущая задержка +servers.manager.server.players=Онлайн +servers.manager.add.accept=Добавить +servers.manager.add.still=Добавить +servers.manager.error.status=Не удалось получить текущее состояние сервера +servers.manager.status=Статус сервера +servers.manager.status.head=Статус сервера \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 1c293a0c607..d70d6cab578 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -205,7 +205,7 @@ archive.version=Версія assets.download=Завантаження ресурсів assets.download_all=Перевірка цілісності ресурсів assets.index.malformed=Індексні файли завантажених ресурсів пошкоджені. Ви можете вирішити цю проблему, натиснувши "Керування → Оновити ресурси гри" на сторінці "Редагувати екземпляр". - +button.back=Назад button.cancel=Скасувати button.change_source=Змінити джерело завантаження button.clear=Очистити @@ -1778,4 +1778,16 @@ servers.manage.copy.server.ip.ok.toast=IP-адреса сервера скопі server.delete=Видалити цей сервер server.tag.hide=Приховане server.tag.hold.current=ВласнийПоточний -server.empty=Немає Сервери \ No newline at end of file +server.empty=Немає Сервери +servers.manager.add=Додати сервер +servers.manager.add.head=Додати сервер +servers.manager.server.name=Назва сервера +servers.manager.server.name.def=Minecraft Server +servers.manager.server.ip=IP-адреса сервера +servers.manager.server.latency=Поточна затримка +servers.manager.server.players=Онлайн +servers.manager.add.accept=Додати +servers.manager.add.still=Додати +servers.manager.error.status=Не вдалося отримати поточний стан сервера +servers.manager.status=Статус сервера +servers.manager.status.head=Статус сервера \ No newline at end of file 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 f75c5e93ce7..d803bc233eb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -204,7 +204,7 @@ archive.version=版本 assets.download=下载资源 assets.download_all=检查资源文件完整性 assets.index.malformed=资源文件的索引文件损坏。你可以在相应实例的“实例管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。 - +button.back=返回 button.cancel=取消 button.change_source=切换下载源 button.clear=清除 @@ -1639,11 +1639,16 @@ server.delete=删除此服务器 server.tag.hide=隐藏 server.tag.hold.current=当前实例 server.empty=没有服务器 + servers.manager.add=添加服务器 servers.manager.add.head=添加服务器 -servers.manager.add.server.name=服务器名字 -servers.manager.add.server.name.def=Minecraft 服务器 -servers.manager.add.server.ip=服务器IP +servers.manager.server.name=服务器名字 +servers.manager.server.name.def=Minecraft 服务器 +servers.manager.server.ip=服务器IP +servers.manager.server.latency=当前延迟 +servers.manager.server.players=在线人数 servers.manager.add.accept=添加 servers.manager.add.still=依然添加 -servers.manager.add.error.status=无法获取目标服务器状态 \ No newline at end of file +servers.manager.error.status=无法获取目标服务器状态 +servers.manager.status=服务器状态 +servers.manager.status.head=服务器状态 \ No newline at end of file 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 a82a4b49aec..a6c1b011545 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -202,7 +202,7 @@ archive.version=版本 assets.download=下載資源 assets.download_all=驗證資源檔案完整性 assets.index.malformed=資源檔案的索引檔案損壞。你可以在相應實例的「實例管理」頁面中,點擊頁面左下角的「設定 → 更新遊戲資源檔案」以修復該問題。 - +button.back=返回 button.cancel=取消 button.change_source=切換下載源 button.clear=清除 @@ -1633,11 +1633,15 @@ server.delete=刪除此伺服器 server.tag.hide=隱藏 server.tag.hold.current=目前實例 server.empty=沒有伺服器 + servers.manager.add=新增伺服器 servers.manager.add.head=新增伺服器 -servers.manager.add.server.name=伺服器名稱 -servers.manager.add.server.name.def=Minecraft 伺服器 +servers.manager.server.name=伺服器名稱 +servers.manager.server.name.def=Minecraft 伺服器 servers.manager.add.server.ip=伺服器位址 +servers.manager.server.latency=當前延遲 +servers.manager.server.players=線上人數 servers.manager.add.accept=新增 servers.manager.add.still=依然新增 -servers.manager.add.error.status=無法獲取目標伺服器狀態 \ No newline at end of file +servers.manager.add.error.status=無法獲取目標伺服器狀態 +servers.manager.status=伺服器狀態 \ No newline at end of file From b9a32e0f9d32dacacbf4222bfee2099c7c6d370d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 23 Sep 2026 23:19:45 +0800 Subject: [PATCH 07/97] update I18N_zh_Hant.properties --- .../src/main/resources/assets/lang/I18N_zh_Hant.properties | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 a6c1b011545..6cf8c6844a8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1638,10 +1638,11 @@ servers.manager.add=新增伺服器 servers.manager.add.head=新增伺服器 servers.manager.server.name=伺服器名稱 servers.manager.server.name.def=Minecraft 伺服器 -servers.manager.add.server.ip=伺服器位址 +servers.manager.server.ip=伺服器位址 servers.manager.server.latency=當前延遲 servers.manager.server.players=線上人數 servers.manager.add.accept=新增 servers.manager.add.still=依然新增 -servers.manager.add.error.status=無法獲取目標伺服器狀態 -servers.manager.status=伺服器狀態 \ No newline at end of file +servers.manager.error.status=無法獲取目標伺服器狀態 +servers.manager.status=伺服器狀態 +servers.manager.status.head=伺服器狀態 \ No newline at end of file From eadfaedbb42e693d42c6145837eec99ae65369aa Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 25 Sep 2026 02:32:01 +0800 Subject: [PATCH 08/97] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/AddServerPane.java | 2 +- .../ui/instances/server/ServerStatusPane.java | 32 +++++++++++++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java index c0f64f49f41..b12caf1e228 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java @@ -177,7 +177,7 @@ private void onAdd() { btnCancel.setOnAction(e -> { initPaneContents(); }); - lblErrorMessage.setText(i18n("servers.manager.add.error.status")); + lblErrorMessage.setText(i18n("servers.manager.error.status")); } else { fireEvent(new DialogCloseEvent()); doAddServer.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 4c60f24106e..74bc661ead8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -50,6 +50,10 @@ public class ServerStatusPane extends TransitionPane implements DialogAware { private @Nullable ServerStatus status; public ServerStatusPane(ServerListPage.IconedServer iconedServer) { + this(iconedServer, false); + } + + public ServerStatusPane(ServerListPage.IconedServer iconedServer, boolean fromDnD) { this.iconedServer = iconedServer; getStyleClass().add("skin-pane"); @@ -64,14 +68,28 @@ public ServerStatusPane(ServerListPage.IconedServer iconedServer) { refreshSpinner.setContent(refreshBtn); } - JFXButton backBtn = new JFXButton(i18n("button.back")); - backBtn.getStyleClass().add("dialog-back"); - backBtn.setOnAction(e -> onClose()); - onEscPressed(this, backBtn::fire); - - HBox actions = new HBox(refreshSpinner, backBtn); + HBox actions = new HBox(refreshSpinner); actions.setAlignment(Pos.CENTER_RIGHT); + if (fromDnD) { + // add to current instance + + // launch game + + JFXButton cancelBtn = new JFXButton(i18n("button.cancel")); + cancelBtn.getStyleClass().add("dialog-cancel"); + cancelBtn.setOnAction(e -> onClose()); + onEscPressed(this, cancelBtn::fire); + actions.getChildren().add(cancelBtn); + } else { + // back + JFXButton backBtn = new JFXButton(i18n("button.back")); + backBtn.getStyleClass().add("dialog-back"); + backBtn.setOnAction(e -> onClose()); + onEscPressed(this, backBtn::fire); + actions.getChildren().add(backBtn); + } + replaceBody(); lblErrorMessage.setWrapText(true); @@ -131,7 +149,7 @@ public void replaceBody() { rows++; textPane.add(new Label(i18n("servers.manager.server.latency") + ":"), 0, rows); - textPane.add(new Label(String.format("%,d", cachedServerStatus.networkLatency())), 1, rows); + textPane.add(new Label(String.format("%,dms", cachedServerStatus.networkLatency())), 1, rows); rows++; textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); From c13f89f64f02281a57428d535b8fa6647a814ae3 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 25 Sep 2026 10:35:02 +0800 Subject: [PATCH 09/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=93=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=20servers.dat=20=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E8=AF=95=E5=9B=BE=E5=9C=A8=E5=90=AF=E5=8A=A8=E5=99=A8?= =?UTF-8?q?=E5=86=85=E6=B7=BB=E5=8A=A0=E3=80=81=E5=A4=8D=E5=88=B6=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E4=BC=9A=E5=A4=B1=E8=B4=A5=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerListPage.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index db7a185a8ff..a03cb9e3b09 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -129,9 +129,15 @@ public void addServer(Server server) { if (gameInstance == null) return; Task.runAsync(Schedulers.io(), () -> { - List servers = Server.loadFromServersDat(gameInstance.getServersDatFilePath()); + Path serversDatFilePath = gameInstance.getServersDatFilePath(); + List servers; + if (Files.exists(serversDatFilePath)) { + servers = Server.loadFromServersDat(serversDatFilePath); + } else { + servers = new ArrayList<>(); + } servers.add(server); - Server.saveToServersDat(servers, gameInstance.getServersDatFilePath()); + Server.saveToServersDat(servers, serversDatFilePath); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); From ee3dc67780e2c562c8d88125c7d3571a0ffca9cb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 25 Sep 2026 10:46:23 +0800 Subject: [PATCH 10/97] =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=A1=B5=E5=BC=BA?= =?UTF-8?q?=E5=88=B6=E4=BD=BF=E7=94=A8=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E6=89=80=E4=BD=BF=E7=94=A8=E7=9A=84=E6=9C=80=E6=96=B0?= =?UTF-8?q?=E5=9B=BE=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 22 +++++++++---------- .../ui/instances/server/ServerStatusPane.java | 8 +------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index a03cb9e3b09..e29bef4b7db 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -403,8 +403,7 @@ public static class IconedServer extends Server { public IconedServer(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { super(acceptTextures, hidden, icon, ip, name); - Image parsedImage = parseImage(icon); - iconImage = parsedImage == null ? FXUtils.newBuiltinImage("/assets/img/unknown_server.png") : parsedImage; + iconImage = parseImageOrDefault(icon); } public static IconedServer pack(Server server) { @@ -420,17 +419,16 @@ public static IconedServer pack(Server server) { ); } - public static @Nullable Image parseImage(String imageBase64String) { - if (imageBase64String == null || imageBase64String.isEmpty()) { - return null; - } - try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { - // png format. - return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); - } catch (Exception e) { - LOG.warning("Failed to decode server icon", e); - return null; + public static @NotNull Image parseImageOrDefault(@Nullable String imageBase64String) { + if (imageBase64String != null && !imageBase64String.isEmpty()) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { + // png format. + return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); + } catch (Exception e) { + LOG.warning("Failed to decode server icon", e); + } } + return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 74bc661ead8..d9b6abae670 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -106,13 +106,7 @@ public void replaceBody() { Image serverIcon = iconedServer.iconImage; if (status != null) { - String latestFavicon = status.favicon(); - if (latestFavicon != null) { - Image latestFaviconImage = ServerListPage.IconedServer.parseImage(latestFavicon); - if (latestFaviconImage != null) { - serverIcon = latestFaviconImage; - } - } + serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); } int iconScale = 4; From b15546651bad1deb81e30027e2741b689db6d487 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 26 Sep 2026 23:49:32 +0800 Subject: [PATCH 11/97] =?UTF-8?q?=E5=88=97=E8=A1=A8=E9=A1=B5=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=BB=B6=E8=BF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/jackhuang/hmcl/ui/SVG.java | 6 + .../ui/instances/server/AddServerPane.java | 4 +- .../ui/instances/server/ServerListPage.java | 121 ++++++++++++------ .../server/ServerNetworkLatencyPane.java | 112 ++++++++++++++++ .../ui/instances/server/ServerStatusPane.java | 12 +- .../org/jackhuang/hmcl/server/Server.java | 11 ++ .../jackhuang/hmcl/server/ServerAddress.java | 20 +++ .../jackhuang/hmcl/server/ServerStatus.java | 4 - .../hmcl/server/ServerStatusGetter.java | 15 ++- .../hmcl/server/ServerStatusResult.java | 60 +++++++++ 10 files changed, 311 insertions(+), 54 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 9991ac13b85..1e82668fa2f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -112,6 +112,12 @@ public enum SVG { SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"), SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), + SERVER_SIGNAL_0_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5"), + SERVER_SIGNAL_1_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5"), // Not Material + SERVER_SIGNAL_2_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5 m4.75 0v-8q0-.625.438-1.062T12 9t1.063.438t.437 1.062v8q0 .625-.437 1.063T12 20t-1.062-.437T10.5 18.5"), // Not Material + SERVER_SIGNAL_3_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5 m4.75 0v-8q0-.625.438-1.062T12 9t1.063.438t.437 1.062v8q0 .625-.437 1.063T12 20t-1.062-.437T10.5 18.5 m4.75 0v-10q0-.625.438-1.062T16.75 7t1.063.438t.437 1.062v10q0 .625-.437 1.063T16.75 20t-1.062-.437t-.438-1.063"), // Not Material + SERVER_SIGNAL_FULL("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5 m4.75 0v-8q0-.625.438-1.062T12 9t1.063.438t.437 1.062v8q0 .625-.437 1.063T12 20t-1.062-.437T10.5 18.5 m4.75 0v-10q0-.625.438-1.062T16.75 7t1.063.438t.437 1.062v10q0 .625-.437 1.063T16.75 20t-1.062-.437t-.438-1.063m4.75 0v-13q0-.625.438-1.062T21.5 4t1.063.438T23 5.5v13q0 .625-.437 1.063T21.5 20t-1.062-.437T20 18.5"), // Not Material + SERVER_SIGNAL_ERROR("M10.5 18.5V14L1.75 7.875q-.425-.3-.513-.8t.213-.925t.812-.525t.938.2l17.6 12.35q.425.3.525.8t-.2.925t-.812.513t-.938-.213l-2.55-1.775h1.425v.075q0 .625-.437 1.063T16.75 20t-1.062-.437t-.438-1.063v-1.175L13.5 16.1v2.325q0 .675-.437 1.125T12 20t-1.062-.437T10.5 18.5m-9.5 0v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5m12.5-5.175l-3-2.1V8.5q0-.625.438-1.062T16.75 7t1.063.438t.437 1.062zM23 16.65l-3-2.1V5.5q0-.625.438-1.062T21.5 4t1.063.438T23 5.5z"), // Not Material SETTINGS("M19.43 12.98C19.47 12.66 19.5 12.34 19.5 12 19.5 11.66 19.47 11.34 19.43 11.02L21.54 9.37C21.73 9.22 21.78 8.95 21.66 8.73L19.66 5.27C19.57 5.11 19.4 5.02 19.22 5.02 19.16 5.02 19.1 5.03 19.05 5.05L16.56 6.05C16.04 5.65 15.48 5.32 14.87 5.07L14.49 2.42C14.46 2.18 14.25 2 14 2H10C9.75 2 9.54 2.18 9.51 2.42L9.13 5.07C8.52 5.32 7.96 5.66 7.44 6.05L4.95 5.05C4.89 5.03 4.83 5.02 4.77 5.02 4.6 5.02 4.43 5.11 4.34 5.27L2.34 8.73C2.21 8.95 2.27 9.22 2.46 9.37L4.57 11.02C4.53 11.34 4.5 11.67 4.5 12 4.5 12.33 4.53 12.66 4.57 12.98L2.46 14.63C2.27 14.78 2.22 15.05 2.34 15.27L4.34 18.73C4.43 18.89 4.6 18.98 4.78 18.98 4.84 18.98 4.9 18.97 4.95 18.95L7.44 17.95C7.96 18.35 8.52 18.68 9.13 18.93L9.51 21.58C9.54 21.82 9.75 22 10 22H14C14.25 22 14.46 21.82 14.49 21.58L14.87 18.93C15.48 18.68 16.04 18.34 16.56 17.95L19.05 18.95C19.11 18.97 19.17 18.98 19.23 18.98 19.4 18.98 19.57 18.89 19.66 18.73L21.66 15.27C21.78 15.05 21.73 14.78 21.54 14.63L19.43 12.98ZM17.45 11.27C17.49 11.58 17.5 11.79 17.5 12 17.5 12.21 17.48 12.43 17.45 12.73L17.31 13.86 18.2 14.56 19.28 15.4 18.58 16.61 17.31 16.1 16.27 15.68 15.37 16.36C14.94 16.68 14.53 16.92 14.12 17.09L13.06 17.52 12.9 18.65 12.7 20H11.3L11.11 18.65 10.95 17.52 9.89 17.09C9.46 16.91 9.06 16.68 8.66 16.38L7.75 15.68 6.69 16.11 5.42 16.62 4.72 15.41 5.8 14.57 6.69 13.87 6.55 12.74C6.52 12.43 6.5 12.2 6.5 12S6.52 11.57 6.55 11.27L6.69 10.14 5.8 9.44 4.72 8.6 5.42 7.39 6.69 7.9 7.73 8.32 8.63 7.64C9.06 7.32 9.47 7.08 9.88 6.91L10.94 6.48 11.1 5.35 11.3 4H12.69L12.88 5.35 13.04 6.48 14.1 6.91C14.53 7.09 14.93 7.32 15.33 7.62L16.24 8.32 17.3 7.89 18.57 7.38 19.27 8.59 18.2 9.44 17.31 10.14 17.45 11.27ZM12 8C9.79 8 8 9.79 8 12S9.79 16 12 16 16 14.21 16 12 14.21 8 12 8ZM12 14C10.9 14 10 13.1 10 12S10.9 10 12 10 14 10.9 14 12 13.1 14 12 14Z"), // Material Icons SETTINGS_FILL("M9.25 22l-.4-3.2q-.325-.125-.6125-.3t-.5625-.375L4.7 19.375l-2.75-4.75 2.575-1.95Q4.5 12.5 4.5 12.3375v-.675q0-.1625.025-.3375L1.95 9.375 4.7 4.625l2.975 1.25q.275-.2.575-.375t.6-.3L9.25 2h5.5l.4 3.2q.325.125.6125.3t.5625.375L19.3 4.625l2.75 4.75-2.575 1.95q.025.175.025.3375v.675q0 .1625-.05.3375l2.575 1.95-2.75 4.75-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2H9.25Zm2.8-6.5q1.45 0 2.475-1.025T15.55 12 14.525 9.525 12.05 8.5q-1.475 0-2.4875 1.025T8.55 12q0 1.45 1.0125 2.475T12.05 15.5Z"), // Material Icons STADIA_CONTROLLER("M4.725 20Q3.225 20 2.1625 18.925T1.05 16.325Q1.05 16.1 1.075 15.875T1.15 15.425L3.25 7.025Q3.6 5.675 4.675 4.8375T7.125 4H16.875Q18.25 4 19.325 4.8375T20.75 7.025L22.85 15.425Q22.9 15.65 22.9375 15.8875T22.975 16.35Q22.975 17.875 21.8875 18.9375T19.275 20Q18.225 20 17.325 19.45T15.975 17.95L15.275 16.5Q15.15 16.25 14.9 16.125T14.375 16H9.625Q9.35 16 9.1 16.125T8.725 16.5L8.025 17.95Q7.575 18.9 6.675 19.45T4.725 20ZM4.8 18Q5.275 18 5.6625 17.75T6.25 17.075L6.95 15.65Q7.325 14.875 8.05 14.4375T9.625 14H14.375Q15.225 14 15.95 14.45T17.075 15.65L17.775 17.075Q17.975 17.5 18.3625 17.75T19.225 18Q19.925 18 20.425 17.5375T20.95 16.375Q20.95 16.4 20.9 15.9L18.8 7.525Q18.625 6.85 18.1 6.425T16.875 6H7.125Q6.425 6 5.8875 6.425T5.2 7.525L3.1 15.9Q3.05 16.05 3.05 16.35 3.05 17.05 3.5625 17.525T4.8 18ZM13.5 11Q13.925 11 14.2125 10.7125T14.5 10Q14.5 9.575 14.2125 9.2875T13.5 9Q13.075 9 12.7875 9.2875T12.5 10Q12.5 10.425 12.7875 10.7125T13.5 11ZM15.5 9Q15.925 9 16.2125 8.7125T16.5 8Q16.5 7.575 16.2125 7.2875T15.5 7Q15.075 7 14.7875 7.2875T14.5 8Q14.5 8.425 14.7875 8.7125T15.5 9ZM15.5 13Q15.925 13 16.2125 12.7125T16.5 12Q16.5 11.575 16.2125 11.2875T15.5 11Q15.075 11 14.7875 11.2875T14.5 12Q14.5 12.425 14.7875 12.7125T15.5 13ZM17.5 11Q17.925 11 18.2125 10.7125T18.5 10Q18.5 9.575 18.2125 9.2875T17.5 9Q17.075 9 16.7875 9.2875T16.5 10Q16.5 10.425 16.7875 10.7125T17.5 11ZM8.5 12.5Q8.825 12.5 9.0375 12.2875T9.25 11.75V10.75H10.25Q10.575 10.75 10.7875 10.5375T11 10Q11 9.675 10.7875 9.4625T10.25 9.25H9.25V8.25Q9.25 7.925 9.0375 7.7125T8.5 7.5Q8.175 7.5 7.9625 7.7125T7.75 8.25V9.25H6.75Q6.425 9.25 6.2125 9.4625T6 10Q6 10.325 6.2125 10.5375T6.75 10.75H7.75V11.75Q7.75 12.075 7.9625 12.2875T8.5 12.5ZM12 12Z"), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java index b12caf1e228..5568c47d0a9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java @@ -26,6 +26,7 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.*; +import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusGetter; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -163,10 +164,11 @@ private void onAdd() { Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(serverIP) - ).whenComplete(Schedulers.javafx(), (status, exception) -> { + ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) LOG.warning("Failed to fetch server status.", exception); + ServerStatus status = result.getIfSuccess(); if (status == null) { spinner.hideSpinner(); btnAccept.setText(i18n("servers.manager.add.still")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index e29bef4b7db..54abcc37374 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -30,15 +31,18 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.Skin; -import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.util.Subscription; import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; +import org.jackhuang.hmcl.server.ServerStatus; +import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; @@ -51,6 +55,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -233,10 +238,11 @@ private static final class ServerListCell extends ListCell { + StackPane statusPane = new StackPane(); + JFXButton statusBtn = FXUtils.newToggleButton4(SVG.NONE); + statusBtn.managedProperty().bind(statusBtn.visibleProperty()); + statusBtn.setOnAction(event -> { ServerHolder holder = getItem(); if (holder != null) - page.launchAndEnterServer(holder); + page.showServerStatus(holder); }); + serverNetworkLatencyPane = new ServerNetworkLatencyPane(); + + statusPane.getChildren().add(serverNetworkLatencyPane); + statusPane.getChildren().add(statusBtn); + right.getChildren().add(statusPane); JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); right.getChildren().add(btnMore); @@ -307,6 +316,11 @@ protected void updateItem(ServerHolder holder, boolean empty) { super.updateItem(holder, empty); + if (serverNetworkLatencyValueSubscription != null) { + serverNetworkLatencyValueSubscription.unsubscribe(); + serverNetworkLatencyValueSubscription = null; + } + if (oldHolder == holder && oldEmpty == empty) return; this.graphic.releaseRippleImmediately(); @@ -314,13 +328,12 @@ protected void updateItem(ServerHolder holder, boolean empty) { if (empty || holder == null) { setGraphic(null); - imageView.setImage(null); - leftTooltip.setText(""); + serverIcon.setImage(null); content.setTitle(""); content.setSubtitle(""); + serverNetworkLatencyPane.error(); } else { - imageView.setImage(holder.server.iconImage); - leftTooltip.setText(holder.server.getIp()); + serverIcon.setImage(holder.server.iconImage); content.setTitle(holder.server.getName() != null ? parseColorEscapes(holder.server.getName()) : ""); content.setSubtitle(holder.server.getIp()); @@ -340,24 +353,39 @@ protected void updateItem(ServerHolder holder, boolean empty) { .forEach(content::addTag); } - // add status tag. -// Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(holder.server.getIp())) -// .whenComplete(Schedulers.javafx(), (status, throwable) -> { -// if (throwable != null) { -// LOG.error("Failed to get server status.", throwable); -// } -// -// if (status != null) { -// if (status.favicon() != null) { -// Image latestImage = IconedServer.parseImage(status.favicon()); -// if (latestImage != null) { -// imageView.setImage(latestImage); -// } -// } -// } -// }).start(); - setGraphic(graphic); + + holder.startGetServerStatusAsync(); + applyServerStatusResult(holder, holder.serverStatusResultWrapper.getReadOnlyProperty().get()); + serverNetworkLatencyValueSubscription = holder.serverStatusResultWrapper.subscribe(result -> applyServerStatusResult(holder, result)); + } + } + + private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { + if (result == null) { + serverNetworkLatencyPane.ping(); + return; + } + ServerStatus serverStatus = result.getIfSuccess(); + if (serverStatus == null) { + serverNetworkLatencyPane.error(); + } else { + serverNetworkLatencyPane.pong(serverStatus.networkLatency()); + + // update latest server icon + serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); + if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { + // save latest icon + Task.runAsync(Schedulers.io(), () -> { + try { + IconedServer newServer = holder.server.withIcon(serverStatus.favicon()); + holder.cachedServers.set(holder.inDatPathSlot, newServer); + Server.saveToServersDat(holder.cachedServers, holder.fromServersDatFilePath); + } catch (Exception e) { + LOG.error("Failed to save servers dat", e); + } + }).start(); + } } } @@ -369,7 +397,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); popupMenu.getContent().addAll( - new IconedMenuItem(SVG.CHAT, i18n("servers.manager.status"), () -> + new IconedMenuItem(SVG.SERVER_SIGNAL_FULL, i18n("servers.manager.status"), () -> page.showServerStatus(holder), popup ), new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_connect_server"), () -> @@ -406,6 +434,10 @@ public IconedServer(boolean acceptTextures, boolean hidden, @Nullable String ico iconImage = parseImageOrDefault(icon); } + public IconedServer withIcon(@Nullable String newIcon) { + return new IconedServer(isAcceptTextures(), isHidden(), newIcon, getIp(), getName()); + } + public static IconedServer pack(Server server) { if (server instanceof IconedServer) { return (IconedServer) server; @@ -439,6 +471,23 @@ public static final class ServerHolder { final int inDatPathSlot; final @NotNull IconedServer server; + final @NotNull ReadOnlyObjectWrapper serverStatusResultWrapper = new ReadOnlyObjectWrapper<>(null); + private final AtomicBoolean serverStatusComputed = new AtomicBoolean(false); + + public void startGetServerStatusAsync() { + if (serverStatusResultWrapper.get() != null) return; + if (serverStatusComputed.compareAndSet(false, true)) { + reGetServerStatusAsync(); + } + } + + public void reGetServerStatusAsync() { + Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(server.getIp())) + .whenComplete(Schedulers.javafx(), (result, ignored) -> { + serverStatusResultWrapper.set(result); + }).start(); + } + public ServerHolder(@NotNull Path fromServersDatFilePath, @NotNull List cachedServers, int inDatPathSlot, @NotNull IconedServer server) { this.fromServersDatFilePath = fromServersDatFilePath; this.inDatPathSlot = inDatPathSlot; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java new file mode 100644 index 00000000000..52cf2718799 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java @@ -0,0 +1,112 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances.server; + +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.SVG; + +import java.util.*; + +public class ServerNetworkLatencyPane extends StackPane { + private static final Set INSTANCES = Collections.newSetFromMap(new WeakHashMap<>()); + private static final Timeline TIMELINE = new Timeline(); + + static { + TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis(200), e -> { + for (ServerNetworkLatencyPane c : List.copyOf(INSTANCES)) { + if (c.state == State.PINGING) { + c.next(); + } + } + })); + TIMELINE.setCycleCount(Animation.INDEFINITE); + TIMELINE.play(); + } + + private final List pingAnimationItems; + private int index = 0; + private State state = State.PINGING; + + public ServerNetworkLatencyPane() { + INSTANCES.add(this); + this.pingAnimationItems = Arrays.asList( + SVG.SERVER_SIGNAL_0_BAR.createIcon(), + SVG.SERVER_SIGNAL_1_BAR.createIcon(), + SVG.SERVER_SIGNAL_2_BAR.createIcon(), + SVG.SERVER_SIGNAL_3_BAR.createIcon(), + SVG.SERVER_SIGNAL_FULL.createIcon(), + SVG.SERVER_SIGNAL_3_BAR.createIcon(), + SVG.SERVER_SIGNAL_2_BAR.createIcon(), + SVG.SERVER_SIGNAL_1_BAR.createIcon() + ); + this.pingAnimationItems.forEach(c -> { + c.setOpacity(0.3); + c.setManaged(false); + c.setVisible(false); + }); + } + + private void next() { + Node currentNode = pingAnimationItems.get(index++ % pingAnimationItems.size()); + Node nextNode = pingAnimationItems.get(index % pingAnimationItems.size()); + + currentNode.setVisible(false); + currentNode.setManaged(false); + nextNode.setVisible(true); + nextNode.setManaged(true); + } + + public void error() { + state = State.ERROR; + getChildren().clear(); + getChildren().add(SVG.SERVER_SIGNAL_ERROR.createIcon()); + } + + public void pong(long networkLatency) { + state = State.PONG; + getChildren().clear(); + if (networkLatency < 150) { + getChildren().add(SVG.SERVER_SIGNAL_FULL.createIcon()); + } else if (networkLatency < 300) { + getChildren().add(SVG.SERVER_SIGNAL_3_BAR.createIcon()); + } else if (networkLatency < 600) { + getChildren().add(SVG.SERVER_SIGNAL_2_BAR.createIcon()); + } else if (networkLatency < 1000) { + getChildren().add(SVG.SERVER_SIGNAL_1_BAR.createIcon()); + } else { + getChildren().add(SVG.SERVER_SIGNAL_0_BAR.createIcon()); + } + } + + public void ping() { + state = State.PINGING; + getChildren().clear(); + getChildren().addAll(pingAnimationItems); + } + + private enum State { + PINGING, + PONG, + ERROR; + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index d9b6abae670..fc7096bc035 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -39,7 +39,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class ServerStatusPane extends TransitionPane implements DialogAware { private final ServerListPage.IconedServer iconedServer; @@ -147,7 +146,7 @@ public void replaceBody() { rows++; textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); - textPane.add(new Label(cachedServerStatus.playerOnline() + "/" + cachedServerStatus.playerMax()), 1, rows); + textPane.add(new Label(String.format("%,d/%,d", cachedServerStatus.playerOnline(), cachedServerStatus.playerMax())), 1, rows); } rootLayout.setBody(contentBox); @@ -158,19 +157,14 @@ private void refresh() { refreshSpinner.showSpinner(); Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(iconedServer.getIp()) - ).whenComplete(Schedulers.javafx(), (status, throwable) -> { + ).whenComplete(Schedulers.javafx(), (result, ignored) -> { refreshSpinner.hideSpinner(); + this.status = result.getIfSuccess(); if (status == null) { lblErrorMessage.setText(i18n("servers.manager.error.status")); } - this.status = status; - - if (throwable != null) { - LOG.error("Error while getting server status.", throwable); - } - replaceBody(); }).start(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 58b8dc75ed3..0d2d84774ee 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -130,4 +130,15 @@ public boolean isHidden() { public @Nullable String getName() { return name; } + + @Override + public String toString() { + return "Server{" + + "acceptTextures=" + acceptTextures + + ", hidden=" + hidden + + ", icon='" + icon + '\'' + + ", ip='" + ip + '\'' + + ", name='" + name + '\'' + + '}'; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java index 4c559962060..5b9201a35f7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java @@ -17,8 +17,11 @@ */ package org.jackhuang.hmcl.server; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.net.InetSocketAddress; +import java.util.Objects; public record ServerAddress( String host, @@ -56,4 +59,21 @@ public static ServerAddress parseAddress(String ipStr) throws IOException { public InetSocketAddress toInetSocketAddress() { return new InetSocketAddress(host, port); } + + @Override + public @NotNull String toString() { + return host + ":" + port; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + ServerAddress that = (ServerAddress) o; + return port == that.port && Objects.equals(host, that.host); + } + + @Override + public int hashCode() { + return Objects.hash(host, port); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 5aa8e58cb4a..e3a7033353e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -20,13 +20,9 @@ import org.jetbrains.annotations.Nullable; public record ServerStatus( - // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Pong_Response long networkLatency, - - // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response int protocol, String protocolName, - int playerMax, int playerOnline, @Nullable String favicon diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java index fec39c64aa7..bff11ccf746 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java @@ -19,22 +19,26 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import org.jetbrains.annotations.NotNull; import java.io.*; import java.net.Socket; import java.net.StandardSocketOptions; import java.nio.charset.StandardCharsets; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +// https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping public final class ServerStatusGetter { private ServerStatusGetter() { } - public static ServerStatus getStatus(String serverIp) throws IOException { + public static @NotNull ServerStatusResult getStatus(String serverIp) throws IOException { return ServerStatusGetter.getStatus(ServerAddress.parseAddress(serverIp)); } - private static ServerStatus getStatus(ServerAddress address) throws IOException { + private static @NotNull ServerStatusResult getStatus(ServerAddress address) throws IOException { ServerAddress resolvedAddress = ServerDnsSrvRedirector.lookup(address); try (Socket socket = new Socket()) { @@ -71,15 +75,18 @@ private static ServerStatus getStatus(ServerAddress address) throws IOException } } - return new ServerStatus( + return ServerStatusResult.success(new ServerStatus( networkLatency, protocol, protocolName, playerMax, playerOnline, favicon - ); + )); } + } catch (Exception e) { + LOG.error("Failed to get the status of server " + address, e); + return ServerStatusResult.failure(e); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java new file mode 100644 index 00000000000..31f0a81b6f0 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -0,0 +1,60 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import org.jetbrains.annotations.Nullable; + +public sealed class ServerStatusResult { + public static ServerStatusResult success(ServerStatus serverStatus) { + return new SuccessResult(serverStatus); + } + + public static ServerStatusResult failure(Exception exception) { + return new FailureResult(exception); + } + + public @Nullable ServerStatus getIfSuccess() { + if (this instanceof SuccessResult success) { + return success.serverStatus; + } + return null; + } + + public @Nullable Exception exceptionIfFailure() { + if (this instanceof FailureResult failure) { + return failure.exception; + } + return null; + } + + private static final class SuccessResult extends ServerStatusResult { + public final ServerStatus serverStatus; + + public SuccessResult(ServerStatus serverStatus) { + this.serverStatus = serverStatus; + } + } + + private static final class FailureResult extends ServerStatusResult { + public final Exception exception; + + public FailureResult(Exception exception) { + this.exception = exception; + } + } +} From 73670e09e697849a60a408a97e5a1a9c1c215076 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 00:58:30 +0800 Subject: [PATCH 12/97] =?UTF-8?q?=E5=9C=A8=E5=88=97=E8=A1=A8=E4=B8=AD?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=BB=B6=E8=BF=9F?= =?UTF-8?q?=E4=BB=A5=E5=8F=8Adnd=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 28 +++---- .../hmcl/ui/instances/GameInstancePage.java | 9 +++ .../hmcl/ui/instances/Instances.java | 5 ++ ...AddServerPane.java => EditServerPane.java} | 61 +++++++++++---- .../ui/instances/server/ServerListPage.java | 28 ++++++- .../server/ServerNetworkLatencyPane.java | 2 +- .../ui/instances/server/ServerStatusPane.java | 64 +++++++++++----- HMCL/src/main/resources/assets/css/root.css | 5 -- .../resources/assets/lang/I18N.properties | 9 ++- .../resources/assets/lang/I18N_ar.properties | 9 ++- .../resources/assets/lang/I18N_de.properties | 9 ++- .../resources/assets/lang/I18N_es.properties | 9 ++- .../resources/assets/lang/I18N_ja.properties | 9 ++- .../resources/assets/lang/I18N_ru.properties | 9 ++- .../resources/assets/lang/I18N_uk.properties | 9 ++- .../assets/lang/I18N_zh_Hans.properties | 9 ++- .../assets/lang/I18N_zh_Hant.properties | 9 ++- .../org/jackhuang/hmcl/server/ServerDnD.java | 76 +++++++++++++++++++ 18 files changed, 293 insertions(+), 66 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/{AddServerPane.java => EditServerPane.java} (76%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index 06deb0b0cf3..bb65f7da5c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -25,14 +25,7 @@ import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.InvalidationListener; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyDoubleProperty; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.*; import javafx.beans.value.ChangeListener; import javafx.event.EventHandler; import javafx.geometry.Insets; @@ -43,24 +36,17 @@ import javafx.scene.Scene; import javafx.scene.effect.BlurType; import javafx.scene.effect.DropShadow; -import javafx.scene.input.DragEvent; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.input.MouseEvent; +import javafx.scene.input.*; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; -import javafx.stage.Screen; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.stage.Window; -import javafx.stage.WindowEvent; +import javafx.stage.*; import javafx.util.Duration; import org.jackhuang.hmcl.Launcher; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD; +import org.jackhuang.hmcl.server.ServerDnD; import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.ui.Controllers; @@ -73,6 +59,8 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.JFXDialogPane; import org.jackhuang.hmcl.ui.construct.Navigator; +import org.jackhuang.hmcl.ui.instances.server.ServerListPage; +import org.jackhuang.hmcl.ui.instances.server.ServerStatusPane; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.WizardProvider; @@ -269,6 +257,10 @@ public Decorator(Node mainPage, Consumer stageInitializer) { root.addEventFilter(DragEvent.DRAG_OVER, AuthlibInjectorDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler( url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url)))); + + root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); + root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( + server -> Controllers.dialog(new ServerStatusPane(ServerListPage.IconedServer.pack(server), true)))); } /// Returns the node that must be installed as the scene root. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 740030826f9..7446e129b18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -24,6 +24,7 @@ import javafx.beans.value.ChangeListener; import javafx.event.Event; import javafx.event.EventType; +import javafx.scene.Node; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.GameInstanceID; @@ -171,6 +172,14 @@ public void showInstanceSettings() { tab.select(gameSettingsTab, false); } + public void showServerList() { + tab.select(serverListTab, false); + } + + public Node getSelectedTab() { + return tab.getSelectionModel().getSelectedItem().getNode(); + } + public void setInstance(GameInstanceID instanceId, HMCLGameRepository repository) { this.instance.set(HMCLGameInstance.Optional.of(repository, instanceId)); } 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 edeccfa29d3..372e4d6ffe6 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 @@ -436,4 +436,9 @@ public static void modifyGameSettings(HMCLGameInstance gameInstance) { Controllers.navigate(Controllers.getGameInstancePage()); } + public static void modifyServerList(HMCLGameInstance gameInstance) { + Controllers.getGameInstancePage().setInstance(gameInstance.getId(), gameInstance.getRepository()); + Controllers.getGameInstancePage().showServerList(); + Controllers.navigate(Controllers.getGameInstancePage()); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java similarity index 76% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 5568c47d0a9..b73fccc0177 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/AddServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -43,9 +43,11 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class AddServerPane extends TransitionPane implements DialogAware { - private final Consumer doAddServer; +public class EditServerPane extends TransitionPane implements DialogAware { + private final Consumer handleCallback; + private final ServerListPage.IconedServer reference; + private final Type type; private final GridPane body = new GridPane(); private final JFXTextField txtServerName = new JFXTextField(); private final JFXTextField txtServerIP = new JFXTextField(); @@ -55,8 +57,10 @@ public class AddServerPane extends TransitionPane implements DialogAware { private final SpinnerPane spinner = new SpinnerPane(); private final Label lblErrorMessage = new Label(); - public AddServerPane(Consumer doAddServer) { - this.doAddServer = doAddServer; + public EditServerPane(Type type, ServerListPage.IconedServer reference, Consumer handleCallback) { + this.type = type; + this.reference = reference; + this.handleCallback = handleCallback; getStyleClass().add("skin-pane"); initPaneContents(); @@ -70,7 +74,10 @@ private void initPaneContents() { JFXDialogLayout rootLayout = new JFXDialogLayout(); getChildren().setAll(rootLayout); - rootLayout.setHeading(new Label(i18n("servers.manager.add.head"))); + rootLayout.setHeading(new Label(switch (type) { + case EDIT -> i18n("servers.manager.edit.head"); + case ADD -> i18n("servers.manager.add.head"); + })); VBox bodyVbox = new VBox(); bodyVbox.setSpacing(8); @@ -96,6 +103,10 @@ private void initPaneContents() { txtServerName.setPromptText(i18n("servers.manager.server.name.def")); body.add(txtServerName, 1, 0); + + if (reference != null) { + txtServerName.setText(reference.getName()); + } } // server ip @@ -108,12 +119,19 @@ private void initPaneContents() { txtServerIP.setValidators(new RequiredValidator()); setValidateWhileTextChanged(txtServerIP, true); body.add(txtServerIP, 1, 1); + + if (reference != null) { + txtServerIP.setText(reference.getIp()); + } } bodyVbox.getChildren().add(body); rootLayout.setBody(bodyVbox); - btnAccept.setText(i18n("servers.manager.add.accept")); + btnAccept.setText(switch (type) { + case EDIT -> i18n("servers.manager.edit.accept"); + case ADD -> i18n("servers.manager.add.accept"); + }); btnAccept.getStyleClass().add("dialog-accept"); btnAccept.setOnAction(e -> onAdd()); btnAccept.disableProperty().bind(new BooleanBinding() { @@ -144,10 +162,6 @@ protected boolean computeValue() { rootLayout.setActions(lblErrorMessage, actions); } - private void onCancel() { - fireEvent(new DialogCloseEvent()); - } - private void onAdd() { spinner.showSpinner(); String serverName; @@ -159,7 +173,6 @@ private void onAdd() { serverName = txtServerName.getText(); } - body.setDisable(true); Task.supplyAsync(Schedulers.io(), () -> @@ -171,10 +184,17 @@ private void onAdd() { ServerStatus status = result.getIfSuccess(); if (status == null) { spinner.hideSpinner(); - btnAccept.setText(i18n("servers.manager.add.still")); + btnAccept.setText(switch (type) { + case EDIT -> i18n("servers.manager.edit.still"); + case ADD -> i18n("servers.manager.add.still"); + }); btnAccept.setOnAction(e -> { fireEvent(new DialogCloseEvent()); - doAddServer.accept(new ServerListPage.IconedServer(false, false, null, serverIP, serverName)); + if (reference != null) { + handleCallback.accept(reference.withIpAndName(serverIP, serverName)); + } else { + handleCallback.accept(new ServerListPage.IconedServer(false, false, null, serverIP, serverName)); + } }); btnCancel.setOnAction(e -> { initPaneContents(); @@ -182,11 +202,24 @@ private void onAdd() { lblErrorMessage.setText(i18n("servers.manager.error.status")); } else { fireEvent(new DialogCloseEvent()); - doAddServer.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); + if (reference != null) { + handleCallback.accept(reference.withIpAndName(serverIP, serverName)); + } else { + handleCallback.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); + } } }).start(); } + private void onCancel() { + fireEvent(new DialogCloseEvent()); + } + + public enum Type { + EDIT, + ADD + } + @Override public void onDialogShown() { txtServerName.requestFocus(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 54abcc37374..1021b4dda84 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -103,6 +103,25 @@ public void showServerStatus(ServerHolder holder) { } } + public void editServer(ServerHolder holder) { + if (gameInstance != null) { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, holder.server, (server) -> { + if (!server.equals(holder.server)) { + Task.runAsync(Schedulers.io(), () -> { + List cachedServers = holder.cachedServers; + cachedServers.set(holder.inDatPathSlot, server); + Server.saveToServersDat(cachedServers, holder.fromServersDatFilePath); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) + LOG.warning("Failed to save server data.", exception); + + refresh(); + }).start(); + } + }))); + } + } + public void copyServerIp(ServerHolder holder) { FXUtils.copyText(holder.server.getIp(), i18n("servers.manage.copy.server.ip.ok.toast")); } @@ -180,7 +199,7 @@ private void updateServerList() { } private void addServer() { - runInFX(() -> Controllers.dialog(new AddServerPane(this::addServer))); + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); } private void refresh() { @@ -397,6 +416,9 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); popupMenu.getContent().addAll( + new IconedMenuItem(SVG.EDIT, i18n("servers.manager.edit"), () -> + page.editServer(holder), popup + ), new IconedMenuItem(SVG.SERVER_SIGNAL_FULL, i18n("servers.manager.status"), () -> page.showServerStatus(holder), popup ), @@ -438,6 +460,10 @@ public IconedServer withIcon(@Nullable String newIcon) { return new IconedServer(isAcceptTextures(), isHidden(), newIcon, getIp(), getName()); } + public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { + return new IconedServer(isAcceptTextures(), isHidden(), getIcon(), newIp, newName); + } + public static IconedServer pack(Server server) { if (server instanceof IconedServer) { return (IconedServer) server; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java index 52cf2718799..1497b9d3301 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java @@ -32,7 +32,7 @@ public class ServerNetworkLatencyPane extends StackPane { private static final Timeline TIMELINE = new Timeline(); static { - TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis(200), e -> { + TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis(125), e -> { for (ServerNetworkLatencyPane c : List.copyOf(INSTANCES)) { if (c.state == State.PINGING) { c.next(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index fc7096bc035..7e5c1cc49ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -26,15 +26,17 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.game.HMCLGameInstance; +import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.setting.GameDirectoryManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.DialogAware; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.ImageContainer; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.ui.construct.*; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jetbrains.annotations.Nullable; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -73,22 +75,48 @@ public ServerStatusPane(ServerListPage.IconedServer iconedServer, boolean fromDn if (fromDnD) { // add to current instance - // launch game - - JFXButton cancelBtn = new JFXButton(i18n("button.cancel")); - cancelBtn.getStyleClass().add("dialog-cancel"); - cancelBtn.setOnAction(e -> onClose()); - onEscPressed(this, cancelBtn::fire); - actions.getChildren().add(cancelBtn); - } else { - // back - JFXButton backBtn = new JFXButton(i18n("button.back")); - backBtn.getStyleClass().add("dialog-back"); - backBtn.setOnAction(e -> onClose()); - onEscPressed(this, backBtn::fire); - actions.getChildren().add(backBtn); + JFXButton addToInstallBtn = new JFXButton(i18n("servers.dnd.add")); + addToInstallBtn.getStyleClass().add("dialog-accept"); + addToInstallBtn.setOnAction(ignored0 -> { + fireEvent(new DialogCloseEvent()); + // open server manager + HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); + HMCLGameInstance selectedInstance = repository.getSelectedInstance(); + if (selectedInstance == null) { + JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download")); + gotoDownload.getStyleClass().add("dialog-accept"); + gotoDownload.setOnAction(ignored1 -> Controllers.navigate(Controllers.getDownloadPage())); + + Controllers.confirmAction(i18n("servers.dnd.add.instanceempty.desc"), i18n("servers.dnd.add.instanceempty"), + MessageDialogPane.MessageType.ERROR, + gotoDownload, + null); + } else { + Instances.modifyServerList(selectedInstance); + if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { + listPage.addServer(iconedServer); + } + } + }); + actions.getChildren().add(addToInstallBtn); } + JFXButton launchBtn = new JFXButton(i18n("instance.launch")); + launchBtn.getStyleClass().add("dialog-accept"); + launchBtn.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); + HMCLGameInstance selectedInstance = repository.getSelectedInstance(); + Instances.launchAndEnterServer(selectedInstance, iconedServer.getIp()); + }); + actions.getChildren().add(launchBtn); + + JFXButton cancelBtn = new JFXButton(i18n("button.cancel")); + cancelBtn.getStyleClass().add("dialog-cancel"); + cancelBtn.setOnAction(e -> onClose()); + onEscPressed(this, cancelBtn::fire); + actions.getChildren().add(cancelBtn); + replaceBody(); lblErrorMessage.setWrapText(true); diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 5bf9bc8a98a..634c20a7248 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -582,11 +582,6 @@ -fx-padding: 0.7em 0.8em; } -.dialog-back { - -fx-text-fill: -monet-on-surface-variant; - -fx-padding: 0.7em 0.8em; -} - /******************************************************************************* * * * Microsoft Login Dialog * diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 973f1a6bf02..52bb118e86c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -204,7 +204,7 @@ archive.version=Version assets.download=Downloading Assets assets.download_all=Validating Assets Integrity assets.index.malformed=Index files of downloaded assets are corrupted. You can resolve this problem by clicking "Manage → Update Game Assets" on the "Edit Instance" page. -button.back=back + button.cancel=Cancel button.change_source=Change Download Source button.clear=Clear @@ -1842,6 +1842,13 @@ servers.manager.server.latency=Current latency servers.manager.server.players=Online servers.manager.add.accept=Add servers.manager.add.still=Still Add +servers.dnd.add=Add to current instance +servers.dnd.add.instanceempty=Add failed +servers.dnd.add.instanceempty.desc=There are no games available to add.\nYou can go to the download page to download a new game, or switch game folders in the "Instance List". +servers.manager.edit=Edit Server +servers.manager.edit.head=Edit Server +servers.manager.edit.accept=Submit +servers.manager.edit.still=Still Submit servers.manager.error.status=Unable to fetch the current server status servers.manager.status=Server Status servers.manager.status.head=Server Status \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index ad199e0df31..330a03b3b45 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -205,7 +205,7 @@ archive.version=الإصدار assets.download=جارٍ تنزيل الأصول assets.download_all=جارٍ التحقق من سلامة الأصول assets.index.malformed=ملفات فهرس الأصول المنزّلة تالفة. يمكنك حل هذه المشكلة بالنقر على "إدارة ← تحديث أصول اللعبة" في صفحة "تعديل النسخة". -button.back=العودة + button.cancel=إلغاء button.change_source=تغيير مصدر التنزيل button.clear=مسح @@ -1839,6 +1839,13 @@ servers.manager.server.latency=التأخير الحالي servers.manager.server.players=على الانترنت servers.manager.add.accept=إضافة servers.manager.add.still=لا تزال إضافة +servers.dnd.add=إضافة إلى هذه الحالة الحالية +servers.dnd.add.instanceempty=فشل الإضافة +servers.dnd.add.instanceempty.desc=لا توجد ألعاب متاحة للإضافة.\nيمكنك الذهاب إلى صفحة التنزيل لتنزيل لعبة جديدة، أو تبديل مجلدات الألعاب في قائمة "المثيلات". +servers.manager.edit=تعديل الخادم +servers.manager.edit.head=تعديل الخادم +servers.manager.edit.accept=إرسال +servers.manager.edit.still=الإرسال على أي حال servers.manager.error.status=غير قادر على الحصول على حالة الخادم الحالية servers.manager.status=حالة الخادم servers.manager.status.head=حالة الخادم \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 0141aef7fc3..001899fcaf0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -203,7 +203,7 @@ archive.version=Version assets.download=Ressourcen werden heruntergeladen assets.download_all=Integrität der Ressourcen wird überprüft assets.index.malformed=Indexdateien der heruntergeladenen Ressourcen sind beschädigt. Sie können dieses Problem beheben, indem Sie auf der Seite "Instanz bearbeiten" auf "Verwalten → Spielressourcen aktualisieren" klicken. -button.back=zurück + button.cancel=Abbrechen button.change_source=Download-Quelle ändern button.clear=Leeren @@ -1841,6 +1841,13 @@ servers.manager.server.latency=Aktuelle Latenz servers.manager.server.players=Online servers.manager.add.accept=Hinzufügen servers.manager.add.still=Noch Hinzufügen +servers.dnd.add=Zur aktuellen Instanz hinzufügen +servers.dnd.add.instanceempty=Hinzufügen fehlgeschlagen +servers.dnd.add.instanceempty.desc=Es gibt keine Spiele zum Hinzufügen.\nDu kannst die Downloadseite besuchen, um ein neues Spiel herunterzuladen, oder den Spielordner in der "Instanzliste" wechseln. +servers.manager.edit=Server bearbeiten +servers.manager.edit.head=Server bearbeiten +servers.manager.edit.accept=Übermitteln +servers.manager.edit.still=Trotzdem Übermitteln servers.manager.error.status=Der aktuelle Serverstatus kann nicht abgerufen werden servers.manager.status=Serverstatus servers.manager.status.head=Serverstatus \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index d8d27a9bfe6..ada1e73f78b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -203,7 +203,7 @@ archive.version=Versión assets.download=Descargando assets assets.download_all=Verificando la integridad de los archivos assets.index.malformed=Los archivos de índice de los activos descargados estaban dañados. Puede intentar utilizar «Actualizar activos del juego» en la configuración de su instancia del juego para solucionar este problema. -button.back=tagasi + button.cancel=Cancelar button.change_source=Cambiar fuente de descarga button.clear=Limpiar @@ -1825,6 +1825,13 @@ servers.manager.server.latency=Praegune viivitus servers.manager.server.players=Internetis servers.manager.add.accept=Lisa servers.manager.add.still=Lisa ikkagi +servers.dnd.add=Añadir a la instancia actual +servers.dnd.add.instanceempty=Error al añadir +servers.dnd.add.instanceempty.desc=No hay juegos disponibles para añadir.\nPuedes ir a la página de descargas para descargar un juego nuevo o cambiar la carpeta del juego en la "Lista de instancias". +servers.manager.edit=Editar servidor +servers.manager.edit.head=Editar servidor +servers.manager.edit.accept=Enviar +servers.manager.edit.still=Enviar igualmente servers.manager.error.status=Serveri praeguse seisundi leidmine nurjus servers.manager.status=Serveri seisund servers.manager.status.head=Serveri seisund \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index e0d3396ad2f..15aa4d73bd8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -192,7 +192,7 @@ archive.version=バージョン assets.download=アセットのダウンロード assets.download_all=アセットの整合性チェック assets.index.malformed=アセットインデックスの形式が正しくありません。バージョン設定の[ゲームアセットファイルの更新]で再試行できます。 -button.back=<戻る + button.cancel=キャンセル button.change_source=ダウンロードソースの変更 button.clear=クリア @@ -1627,6 +1627,13 @@ servers.manager.server.latency=現在の遅延 servers.manager.server.players=オンライン servers.manager.add.accept=追加 servers.manager.add.still=追加 +servers.dnd.add=現在のインスタンスに追加 +servers.dnd.add.instanceempty=追加に失敗しました +servers.dnd.add.instanceempty.desc=追加できるゲームがありません。\nダウンロードページで新しいゲームをダウンロードするか、"インスタンス一覧" でゲームフォルダを切り替えることができます。 +servers.manager.edit=サーバーを編集 +servers.manager.edit.head=サーバーを編集 +servers.manager.edit.accept=送信 +servers.manager.edit.still=強制送信 servers.manager.error.status=現在のサーバステータスを取得できません servers.manager.status=サーバの状態 servers.manager.status.head=サーバの状態 \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 8a16a9da4aa..e6c64837df2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -199,7 +199,7 @@ archive.version=Версия assets.download=Скачивание ассетов assets.download_all=Проверка целостности ассетов assets.index.malformed=Индексные файлы скачанных ассетов повреждены. Чтобы исправить это, нажмите «Инструменты → Обновить ассеты игры» на странице «Изменить сборку». -button.back=назад + button.cancel=Отмена button.change_source=Изменить источник скачивания button.clear=Очистить @@ -1759,6 +1759,13 @@ servers.manager.server.latency=Текущая задержка servers.manager.server.players=Онлайн servers.manager.add.accept=Добавить servers.manager.add.still=Добавить +servers.dnd.add=Добавить в текущий экземпляр +servers.dnd.add.instanceempty=Не удалось добавить +servers.dnd.add.instanceempty.desc=Нет игр, доступных для добавления.\nВы можете перейти на страницу загрузки, чтобы скачать новую игру, или переключить папку игры в списке "Экземпляры". +servers.manager.edit=Редактировать сервер +servers.manager.edit.head=Редактировать сервер +servers.manager.edit.accept=Подтвердить +servers.manager.edit.still=Все равно подтвердить servers.manager.error.status=Не удалось получить текущее состояние сервера servers.manager.status=Статус сервера servers.manager.status.head=Статус сервера \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index d70d6cab578..5fd20b1fa7b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -205,7 +205,7 @@ archive.version=Версія assets.download=Завантаження ресурсів assets.download_all=Перевірка цілісності ресурсів assets.index.malformed=Індексні файли завантажених ресурсів пошкоджені. Ви можете вирішити цю проблему, натиснувши "Керування → Оновити ресурси гри" на сторінці "Редагувати екземпляр". -button.back=Назад + button.cancel=Скасувати button.change_source=Змінити джерело завантаження button.clear=Очистити @@ -1788,6 +1788,13 @@ servers.manager.server.latency=Поточна затримка servers.manager.server.players=Онлайн servers.manager.add.accept=Додати servers.manager.add.still=Додати +servers.dnd.add=Додати до поточного екземпляра +servers.dnd.add.instanceempty=Не вдалося додати +servers.dnd.add.instanceempty.desc=Немає доступних ігор для додавання.\nВи можете перейти на сторінку завантажень, щоб завантажити нову гру, або переключити папку гри в списку "Екземпляри". +servers.manager.edit=Редагувати сервер +servers.manager.edit.head=Редагувати сервер +servers.manager.edit.accept=Підтвердити +servers.manager.edit.still=Все одно підтвердити servers.manager.error.status=Не вдалося отримати поточний стан сервера servers.manager.status=Статус сервера servers.manager.status.head=Статус сервера \ No newline at end of file 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 d803bc233eb..b0bef00c9ee 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -204,7 +204,7 @@ archive.version=版本 assets.download=下载资源 assets.download_all=检查资源文件完整性 assets.index.malformed=资源文件的索引文件损坏。你可以在相应实例的“实例管理”页面中,点击左下角“管理 → 更新游戏资源文件”以修复该问题。\n你可以点击右上角帮助按钮进行求助。 -button.back=返回 + button.cancel=取消 button.change_source=切换下载源 button.clear=清除 @@ -1649,6 +1649,13 @@ servers.manager.server.latency=当前延迟 servers.manager.server.players=在线人数 servers.manager.add.accept=添加 servers.manager.add.still=依然添加 +servers.dnd.add=添加到当前实例 +servers.dnd.add.instanceempty=添加失败 +servers.dnd.add.instanceempty.desc=没有可添加的游戏。\n你可以前往下载页面下载新游戏,或在“实例列表”中切换游戏文件夹。 +servers.manager.edit=编辑服务器 +servers.manager.edit.head=编辑服务器 +servers.manager.edit.accept=提交 +servers.manager.edit.still=依然提交 servers.manager.error.status=无法获取目标服务器状态 servers.manager.status=服务器状态 servers.manager.status.head=服务器状态 \ No newline at end of file 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 6cf8c6844a8..ecb8f809f72 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -202,7 +202,7 @@ archive.version=版本 assets.download=下載資源 assets.download_all=驗證資源檔案完整性 assets.index.malformed=資源檔案的索引檔案損壞。你可以在相應實例的「實例管理」頁面中,點擊頁面左下角的「設定 → 更新遊戲資源檔案」以修復該問題。 -button.back=返回 + button.cancel=取消 button.change_source=切換下載源 button.clear=清除 @@ -1643,6 +1643,13 @@ servers.manager.server.latency=當前延遲 servers.manager.server.players=線上人數 servers.manager.add.accept=新增 servers.manager.add.still=依然新增 +servers.dnd.add=新增到目前實例 +servers.dnd.add.instanceempty=新增失敗 +servers.dnd.add.instanceempty.desc=沒有可新增的遊戲。\n你可以前往下載頁面下載新遊戲,或在「實例列表」中切換遊戲資料夾。 +servers.manager.edit=編輯伺服器 +servers.manager.edit.head=編輯伺服器 +servers.manager.edit.accept=提交 +servers.manager.edit.still=仍然提交 servers.manager.error.status=無法獲取目標伺服器狀態 servers.manager.status=伺服器狀態 servers.manager.status.head=伺服器狀態 \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java new file mode 100644 index 00000000000..5f09ec084ec --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -0,0 +1,76 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import javafx.event.EventHandler; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.TransferMode; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; +import java.util.function.Consumer; + +// minecraft-server:base64ServerName:base64ServerIP[:base64Favicon] +public final class ServerDnD { + private static final String SCHEME = "minecraft-server"; + + private ServerDnD() { + } + + public static Optional parseUrlFromDragboard(Dragboard dragboard) { + String url = dragboard.getString(); + if (url == null) return Optional.empty(); + + + String[] urlElements = url.split(":"); + + if (urlElements.length != 4 && urlElements.length != 3) { + return Optional.empty(); + } + if (!urlElements[0].equals(SCHEME)) return Optional.empty(); + + try { + return Optional.of(new Server( + false, + false, + urlElements.length == 4 ? urlElements[3] : null, + new String(Base64.getDecoder().decode(urlElements[2]), StandardCharsets.UTF_8), + new String(Base64.getDecoder().decode(urlElements[1]), StandardCharsets.UTF_8) + )); + } catch (Exception ignored) { + } + return Optional.empty(); + } + + public static EventHandler dragOverHandler() { + return event -> parseUrlFromDragboard(event.getDragboard()).ifPresent(url -> { + event.acceptTransferModes(TransferMode.COPY); + event.consume(); + }); + } + + public static EventHandler dragDroppedHandler(Consumer onServerTransfered) { + return event -> parseUrlFromDragboard(event.getDragboard()).ifPresent(server -> { + event.setDropCompleted(true); + event.consume(); + onServerTransfered.accept(server); + }); + } +} From 387d40ac70470ffefe99c18915ce4582d7c6de4d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 18:45:40 +0800 Subject: [PATCH 13/97] =?UTF-8?q?=E7=BC=A9=E5=B0=8F=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=A1=86=E9=87=8C=E7=9A=84=E5=9B=BE=E6=A0=87?= =?UTF-8?q?=E5=A4=A7=E5=B0=8F=E4=B8=BA=E5=A4=9A=E4=BA=BA=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E7=9A=84=E5=9B=BE=E6=A0=87=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerStatusPane.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 7e5c1cc49ca..1bc7ef69a1b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -136,26 +136,26 @@ public void replaceBody() { serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); } - int iconScale = 4; - ImageContainer imageView = new ImageContainer(32); + int iconScale = 1; + ImageContainer imageView = new ImageContainer(64); imageView.setImage(serverIcon); imageView.setScaleX(iconScale); imageView.setScaleY(iconScale); StackPane canvasPane = new StackPane(imageView); - canvasPane.setPrefWidth(iconScale * 32); - canvasPane.setPrefHeight(iconScale * 32); + canvasPane.setPrefWidth(iconScale * 64); + canvasPane.setPrefHeight(iconScale * 64); contentBox.getChildren().add(canvasPane); GridPane textPane = new GridPane(); - textPane.setAlignment(Pos.TOP_LEFT); + textPane.setAlignment(Pos.CENTER_LEFT); textPane.setHgap(10); textPane.setVgap(10); contentBox.getChildren().add(textPane); - HBox.setMargin(textPane, new Insets(15, 0, 0, 20)); + HBox.setMargin(textPane, new Insets(0, 0, 0, 20)); int rows = 0; textPane.add(new Label(i18n("servers.manager.server.name") + ":"), 0, rows); From 285f422176dc00744b72c244c61a0fa3d8f5557d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 19:06:02 +0800 Subject: [PATCH 14/97] =?UTF-8?q?=E8=A1=A5=E5=85=85=20Not=20Material=20?= =?UTF-8?q?=E6=A0=87=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java index 1e82668fa2f..460934bafae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/SVG.java @@ -112,7 +112,7 @@ public enum SVG { SCRIPT("M14,20A2,2 0 0,0 16,18V5H9A1,1 0 0,0 8,6V16H5V5A3,3 0 0,1 8,2H19A3,3 0 0,1 22,5V6H18V18L18,19A3,3 0 0,1 15,22H5A3,3 0 0,1 2,19V18H12A2,2 0 0,0 14,20Z"), // Not Material SEARCH("M19.6 21 13.3 14.7Q12.55 15.3 11.575 15.65T9.5 16Q6.775 16 4.8875 14.1125T3 9.5Q3 6.775 4.8875 4.8875T9.5 3Q12.225 3 14.1125 4.8875T16 9.5Q16 10.6 15.65 11.575T14.7 13.3L21 19.6 19.6 21ZM9.5 14Q11.375 14 12.6875 12.6875T14 9.5Q14 7.625 12.6875 6.3125T9.5 5Q7.625 5 6.3125 6.3125T5 9.5Q5 11.375 6.3125 12.6875T9.5 14Z"), SELECT_ALL("M7 17V7H17V17H7ZM9 15H15V9H9V15ZM5 19V21Q4.175 21 3.5875 20.4125T3 19H5ZM3 17V15H5V17H3ZM3 13V11H5V13H3ZM3 9V7H5V9H3ZM5 5H3Q3 4.175 3.5875 3.5875T5 3V5ZM7 21V19H9V21H7ZM7 5V3H9V5H7ZM11 21V19H13V21H11ZM11 5V3H13V5H11ZM15 21V19H17V21H15ZM15 5V3H17V5H15ZM19 21V19H21Q21 19.825 20.4125 20.4125T19 21ZM19 17V15H21V17H19ZM19 13V11H21V13H19ZM19 9V7H21V9H19ZM19 5V3Q19.825 3 20.4125 3.5875T21 5H19Z"), - SERVER_SIGNAL_0_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5"), + SERVER_SIGNAL_0_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5"), // Not Material SERVER_SIGNAL_1_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5"), // Not Material SERVER_SIGNAL_2_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5 m4.75 0v-8q0-.625.438-1.062T12 9t1.063.438t.437 1.062v8q0 .625-.437 1.063T12 20t-1.062-.437T10.5 18.5"), // Not Material SERVER_SIGNAL_3_BAR("M1 18.5v-3q0-.625.438-1.062T2.5 14t1.063.438T4 15.5v3q0 .625-.437 1.063T2.5 20t-1.062-.437T1 18.5 m4.75 0v-5q0-.625.438-1.062T7.25 12t1.063.438t.437 1.062v5q0 .625-.437 1.063T7.25 20t-1.062-.437T5.75 18.5 m4.75 0v-8q0-.625.438-1.062T12 9t1.063.438t.437 1.062v8q0 .625-.437 1.063T12 20t-1.062-.437T10.5 18.5 m4.75 0v-10q0-.625.438-1.062T16.75 7t1.063.438t.437 1.062v10q0 .625-.437 1.063T16.75 20t-1.062-.437t-.438-1.063"), // Not Material From 520457435e2171ca4217724e3c5d37c37bab086d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 20:12:14 +0800 Subject: [PATCH 15/97] =?UTF-8?q?=E8=A1=A5=E5=85=85=20Tooltip=20=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E8=A1=A5=E5=85=85=E6=9B=B4=E5=A4=9A=E7=9A=84=E4=BE=BF?= =?UTF-8?q?=E6=8D=B7=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 33 +++++++++++++++++-- .../resources/assets/lang/I18N.properties | 5 ++- .../resources/assets/lang/I18N_ar.properties | 5 ++- .../resources/assets/lang/I18N_de.properties | 5 ++- .../resources/assets/lang/I18N_es.properties | 5 ++- .../resources/assets/lang/I18N_ja.properties | 5 ++- .../resources/assets/lang/I18N_ru.properties | 5 ++- .../resources/assets/lang/I18N_uk.properties | 5 ++- .../assets/lang/I18N_zh_Hans.properties | 5 ++- .../assets/lang/I18N_zh_Hant.properties | 5 ++- 10 files changed, 67 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 1021b4dda84..17290acf93d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -31,6 +31,7 @@ import javafx.scene.control.Label; import javafx.scene.control.ListCell; import javafx.scene.control.Skin; +import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.input.MouseButton; import javafx.scene.layout.BorderPane; @@ -261,6 +262,9 @@ private static final class ServerListCell extends ListCell { ServerHolder holder = getItem(); @@ -305,6 +309,27 @@ public ServerListCell(ServerListPage page) { statusPane.getChildren().add(statusBtn); right.getChildren().add(statusPane); + statusBtnTooltip = new Tooltip(); + FXUtils.installFastTooltip(statusBtn, statusBtnTooltip); + + JFXButton editBtn = FXUtils.newToggleButton4(SVG.EDIT); + right.getChildren().add(editBtn); + FXUtils.installFastTooltip(editBtn, i18n("servers.manager.edit")); + editBtn.setOnAction(event -> { + ServerHolder holder = getItem(); + if (holder != null) + page.editServer(holder); + }); + + JFXButton launchBtn = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); + right.getChildren().add(launchBtn); + FXUtils.installFastTooltip(launchBtn, i18n("instance.launch")); + launchBtn.setOnAction(event -> { + ServerHolder holder = getItem(); + if (holder != null) + page.launchAndEnterServer(holder); + }); + JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); right.getChildren().add(btnMore); btnMore.setOnAction(event -> { @@ -351,6 +376,7 @@ protected void updateItem(ServerHolder holder, boolean empty) { content.setTitle(""); content.setSubtitle(""); serverNetworkLatencyPane.error(); + statusBtnTooltip.setText(""); } else { serverIcon.setImage(holder.server.iconImage); content.setTitle(holder.server.getName() != null ? parseColorEscapes(holder.server.getName()) : ""); @@ -383,14 +409,16 @@ protected void updateItem(ServerHolder holder, boolean empty) { private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { if (result == null) { serverNetworkLatencyPane.ping(); + statusBtnTooltip.setText(i18n("servers.manager.status.outside.pinging")); return; } ServerStatus serverStatus = result.getIfSuccess(); if (serverStatus == null) { serverNetworkLatencyPane.error(); + statusBtnTooltip.setText(i18n("servers.manager.status.outside.error")); } else { serverNetworkLatencyPane.pong(serverStatus.networkLatency()); - + statusBtnTooltip.setText(i18n("servers.manager.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); // update latest server icon serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { @@ -422,6 +450,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos new IconedMenuItem(SVG.SERVER_SIGNAL_FULL, i18n("servers.manager.status"), () -> page.showServerStatus(holder), popup ), + new MenuSeparator(), new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_connect_server"), () -> page.launchAndEnterServer(holder), popup ), diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 52bb118e86c..402263e17f4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1851,4 +1851,7 @@ servers.manager.edit.accept=Submit servers.manager.edit.still=Still Submit servers.manager.error.status=Unable to fetch the current server status servers.manager.status=Server Status -servers.manager.status.head=Server Status \ No newline at end of file +servers.manager.status.head=Server Status +servers.manager.status.outside.pinging=Server Status (Pinging...) +servers.manager.status.outside.pong=Server Status (%sms) +servers.manager.status.outside.error=Server Status (Failed) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 330a03b3b45..ad0909b5eb7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1848,4 +1848,7 @@ servers.manager.edit.accept=إرسال servers.manager.edit.still=الإرسال على أي حال servers.manager.error.status=غير قادر على الحصول على حالة الخادم الحالية servers.manager.status=حالة الخادم -servers.manager.status.head=حالة الخادم \ No newline at end of file +servers.manager.status.head=حالة الخادم +servers.manager.status.outside.pinging=حالة الخادم (جارٍ الفحص...) +servers.manager.status.outside.pong=حالة الخادم (%sms) +servers.manager.status.outside.error=حالة الخادم (فشل) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 001899fcaf0..7b45e436a68 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1850,4 +1850,7 @@ servers.manager.edit.accept=Übermitteln servers.manager.edit.still=Trotzdem Übermitteln servers.manager.error.status=Der aktuelle Serverstatus kann nicht abgerufen werden servers.manager.status=Serverstatus -servers.manager.status.head=Serverstatus \ No newline at end of file +servers.manager.status.head=Serverstatus +servers.manager.status.outside.pinging=Serverstatus (Pingt...) +servers.manager.status.outside.pong=Serverstatus (%sms) +servers.manager.status.outside.error=Serverstatus (Fehlgeschlagen) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index ada1e73f78b..98301e905b9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1834,4 +1834,7 @@ servers.manager.edit.accept=Enviar servers.manager.edit.still=Enviar igualmente servers.manager.error.status=Serveri praeguse seisundi leidmine nurjus servers.manager.status=Serveri seisund -servers.manager.status.head=Serveri seisund \ No newline at end of file +servers.manager.status.head=Serveri seisund +servers.manager.status.outside.pinging=Serveri seisund (kontrollitakse...) +servers.manager.status.outside.pong=Serveri seisund (%sms) +servers.manager.status.outside.error=Serveri seisund (ebaõnnestus) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 15aa4d73bd8..95f7ab75b1a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1636,4 +1636,7 @@ servers.manager.edit.accept=送信 servers.manager.edit.still=強制送信 servers.manager.error.status=現在のサーバステータスを取得できません servers.manager.status=サーバの状態 -servers.manager.status.head=サーバの状態 \ No newline at end of file +servers.manager.status.head=サーバの状態 +servers.manager.status.outside.pinging=サーバの状態 (Ping中...) +servers.manager.status.outside.pong=サーバの状態 (%sms) +servers.manager.status.outside.error=サーバの状態 (失敗) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e6c64837df2..c0a972b89c7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1768,4 +1768,7 @@ servers.manager.edit.accept=Подтвердить servers.manager.edit.still=Все равно подтвердить servers.manager.error.status=Не удалось получить текущее состояние сервера servers.manager.status=Статус сервера -servers.manager.status.head=Статус сервера \ No newline at end of file +servers.manager.status.head=Статус сервера +servers.manager.status.outside.pinging=Статус сервера (пинг...) +servers.manager.status.outside.pong=Статус сервера (%sms) +servers.manager.status.outside.error=Статус сервера (не удалось) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 5fd20b1fa7b..d18108490b8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1797,4 +1797,7 @@ servers.manager.edit.accept=Підтвердити servers.manager.edit.still=Все одно підтвердити servers.manager.error.status=Не вдалося отримати поточний стан сервера servers.manager.status=Статус сервера -servers.manager.status.head=Статус сервера \ No newline at end of file +servers.manager.status.head=Статус сервера +servers.manager.status.outside.pinging=Статус сервера (пінг...) +servers.manager.status.outside.pong=Статус сервера (%sms) +servers.manager.status.outside.error=Статус сервера (помилка) \ No newline at end of file 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 b0bef00c9ee..0fb3aabd954 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1658,4 +1658,7 @@ servers.manager.edit.accept=提交 servers.manager.edit.still=依然提交 servers.manager.error.status=无法获取目标服务器状态 servers.manager.status=服务器状态 -servers.manager.status.head=服务器状态 \ No newline at end of file +servers.manager.status.head=服务器状态 +servers.manager.status.outside.pinging=服务器状态(Pinging...) +servers.manager.status.outside.pong=服务器状态(%sms) +servers.manager.status.outside.error=服务器状态(失败) \ No newline at end of file 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 ecb8f809f72..e4bdd2bb76d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1652,4 +1652,7 @@ servers.manager.edit.accept=提交 servers.manager.edit.still=仍然提交 servers.manager.error.status=無法獲取目標伺服器狀態 servers.manager.status=伺服器狀態 -servers.manager.status.head=伺服器狀態 \ No newline at end of file +servers.manager.status.head=伺服器狀態 +servers.manager.status.outside.pinging=伺服器狀態 (Pinging...) +servers.manager.status.outside.pong=伺服器狀態 (%sms) +servers.manager.status.outside.error=伺服器狀態 (失敗) \ No newline at end of file From ec0695206c6545081888854111dba9cb2a872d0e Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 21:29:03 +0800 Subject: [PATCH 16/97] =?UTF-8?q?=E8=A1=A5=E5=85=85=20ServerStatus=20?= =?UTF-8?q?=E6=89=80=E6=9C=89=E5=8F=AF=E8=83=BD=E5=AD=98=E5=9C=A8=E7=9A=84?= =?UTF-8?q?=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerStatusPane.java | 2 +- .../jackhuang/hmcl/server/ServerStatus.java | 52 ++++++++- .../hmcl/server/ServerStatusGetter.java | 108 +++++++++++++----- 3 files changed, 130 insertions(+), 32 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 1bc7ef69a1b..d8db9c75b4a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -174,7 +174,7 @@ public void replaceBody() { rows++; textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); - textPane.add(new Label(String.format("%,d/%,d", cachedServerStatus.playerOnline(), cachedServerStatus.playerMax())), 1, rows); + textPane.add(new Label(String.format("%,d/%,d", cachedServerStatus.players().online(), cachedServerStatus.players().max())), 1, rows); } rootLayout.setBody(contentBox); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index e3a7033353e..ec05dd45730 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -17,14 +17,56 @@ */ package org.jackhuang.hmcl.server; +import com.google.gson.JsonElement; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.List; +import java.util.UUID; + public record ServerStatus( long networkLatency, - int protocol, - String protocolName, - int playerMax, - int playerOnline, - @Nullable String favicon + @NotNull Version version, + @NotNull Players players, + @NotNull Description description, + @Nullable String favicon, + boolean enforcesSecureChat, + @Nullable ModInfo modInfo ) { + + public record Version( + @NotNull String name, + int version + ) { + + } + + public record Players( + int max, + int online, + @NotNull List<@NotNull Sample> samples + ) { + public record Sample( + @NotNull String name, + @NotNull UUID id + ) { + + } + } + + public record Description(@NotNull JsonElement description) { + + } + + public record ModInfo( + @NotNull String type, + @NotNull List modList + ) { + public record Mod( + @NotNull String modId, + @NotNull String version + ) { + + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java index bff11ccf746..b02e1188e8c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java @@ -17,18 +17,20 @@ */ package org.jackhuang.hmcl.server; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import com.google.gson.*; import org.jetbrains.annotations.NotNull; import java.io.*; import java.net.Socket; import java.net.StandardSocketOptions; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.UUID; import static org.jackhuang.hmcl.util.logging.Logger.LOG; // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping +// https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Minecraft_Forge_Handshake public final class ServerStatusGetter { private ServerStatusGetter() { @@ -59,30 +61,8 @@ private ServerStatusGetter() { JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); - JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); - int protocol = versionJsonObject.get("protocol").getAsInt(); - String protocolName = versionJsonObject.get("name").getAsString(); - - JsonObject playersJsonObject = rootStatus.getAsJsonObject("players"); - int playerMax = playersJsonObject.get("max").getAsInt(); - int playerOnline = playersJsonObject.get("online").getAsInt(); - - String favicon = null; - if (rootStatus.get("favicon") != null) { - String fetchedFavicon = rootStatus.get("favicon").getAsString(); - if (fetchedFavicon.startsWith("data:image/png;base64,")) { - favicon = fetchedFavicon.substring("data:image/png;base64,".length()); - } - } - - return ServerStatusResult.success(new ServerStatus( - networkLatency, - protocol, - protocolName, - playerMax, - playerOnline, - favicon - )); + ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); + return ServerStatusResult.success(parseServerStatus); } } catch (Exception e) { LOG.error("Failed to get the status of server " + address, e); @@ -90,6 +70,82 @@ private ServerStatusGetter() { } } + private static ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatus) { + JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); + ServerStatus.Version version = new ServerStatus.Version(versionJsonObject.get("name").getAsString(), versionJsonObject.get("protocol").getAsInt()); + + JsonObject playersJsonObject = rootStatus.getAsJsonObject("players"); + ArrayList samples = new ArrayList<>(); + ServerStatus.Players players = new ServerStatus.Players(playersJsonObject.get("max").getAsInt(), playersJsonObject.get("online").getAsInt(), samples); + JsonElement playersSampleJsonElement = playersJsonObject.get("sample"); + if (playersSampleJsonElement instanceof JsonArray jArray) { + for (JsonElement element : jArray) { + JsonObject jsonObject = (JsonObject) element; + samples.add(new ServerStatus.Players.Sample( + jsonObject.get("name").getAsString(), + UUID.fromString(jsonObject.get("id").getAsString()) + )); + } + } + + ServerStatus.Description description; + if (rootStatus.get("description") != null) { + description = new ServerStatus.Description(rootStatus.get("description")); + } else { + description = new ServerStatus.Description(JsonNull.INSTANCE); + } + + String favicon; + if (rootStatus.get("favicon") != null) { + String fetchedFavicon = rootStatus.get("favicon").getAsString(); + if (fetchedFavicon.startsWith("data:image/png;base64,")) { + favicon = fetchedFavicon.substring("data:image/png;base64,".length()); + } else { + favicon = null; + } + } else { + favicon = null; + } + + boolean enforcesSecureChat; + if (rootStatus.get("enforcesSecureChat") != null) { + enforcesSecureChat = rootStatus.get("enforcesSecureChat").getAsBoolean(); + } else { + enforcesSecureChat = false; + } + + ServerStatus.ModInfo modInfo; + if (rootStatus.get("modinfo") != null) { + JsonObject modinfoJsonObject = rootStatus.get("modinfo").getAsJsonObject(); + ArrayList modList = new ArrayList<>(); + modInfo = new ServerStatus.ModInfo( + modinfoJsonObject.get("type").getAsString(), + modList + ); + if (modinfoJsonObject.get("modList") instanceof JsonArray jArray) { + for (JsonElement element : jArray) { + JsonObject jsonObject = (JsonObject) element; + modList.add(new ServerStatus.ModInfo.Mod( + jsonObject.get("modid").getAsString(), + jsonObject.get("version").getAsString() + )); + } + } + } else { + modInfo = null; + } + + return new ServerStatus( + networkLatency, + version, + players, + description, + favicon, + enforcesSecureChat, + modInfo + ); + } + private static long readPongResponsePacket(DataInputStream in) throws IOException { byte[] packetData = new byte[readVarInt(in)]; in.readFully(packetData); From a0512ec282203fc75207ee3dea7fe92b36842660 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 22:04:27 +0800 Subject: [PATCH 17/97] =?UTF-8?q?=E4=BD=BF=E7=94=A8=20ReadOnlyObjectWrappe?= =?UTF-8?q?r=20=E5=8C=85=E8=A3=85=20ServerStatusPane=20=E7=9A=84=20ServerS?= =?UTF-8?q?tatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 2 +- .../ui/instances/server/ServerListPage.java | 2 +- .../ui/instances/server/ServerStatusPane.java | 72 ++++++++++++------- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index bb65f7da5c4..a49e97cfda5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -260,7 +260,7 @@ public Decorator(Node mainPage, Consumer stageInitializer) { root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( - server -> Controllers.dialog(new ServerStatusPane(ServerListPage.IconedServer.pack(server), true)))); + server -> Controllers.dialog(new ServerStatusPane(null, ServerListPage.IconedServer.pack(server), true)))); } /// Returns the node that must be installed as the scene root. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 17290acf93d..6503ec11d36 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -100,7 +100,7 @@ public void launchAndEnterServer(ServerHolder holder) { public void showServerStatus(ServerHolder holder) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.server))); + runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.serverStatusResultWrapper, holder.server))); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index d8db9c75b4a..d4d726082f3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -19,6 +19,9 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.WeakChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; @@ -30,6 +33,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.setting.GameDirectoryManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -37,7 +41,8 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; -import org.jetbrains.annotations.Nullable; + +import java.util.Objects; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -47,15 +52,18 @@ public class ServerStatusPane extends TransitionPane implements DialogAware { private final SpinnerPane refreshSpinner = new SpinnerPane(); private final JFXDialogLayout rootLayout = new JFXDialogLayout(); private final Label lblErrorMessage = new Label(); + private final ReadOnlyObjectWrapper serverStatusResultWrapper; + private final ChangeListener listener = (ignored0, ignored1, newResult) -> { + replaceBody(); + }; - private @Nullable ServerStatus status; - - public ServerStatusPane(ServerListPage.IconedServer iconedServer) { - this(iconedServer, false); + public ServerStatusPane(ReadOnlyObjectWrapper reference, ServerListPage.IconedServer iconedServer) { + this(reference, iconedServer, false); } - public ServerStatusPane(ServerListPage.IconedServer iconedServer, boolean fromDnD) { + public ServerStatusPane(ReadOnlyObjectWrapper reference, ServerListPage.IconedServer iconedServer, boolean fromDnD) { this.iconedServer = iconedServer; + this.serverStatusResultWrapper = Objects.requireNonNullElseGet(reference, () -> new ReadOnlyObjectWrapper<>(null)); getStyleClass().add("skin-pane"); getChildren().setAll(rootLayout); @@ -117,25 +125,43 @@ public ServerStatusPane(ServerListPage.IconedServer iconedServer, boolean fromDn onEscPressed(this, cancelBtn::fire); actions.getChildren().add(cancelBtn); - replaceBody(); - lblErrorMessage.setWrapText(true); lblErrorMessage.setMaxWidth(400); rootLayout.getActions().addAll(lblErrorMessage, actions); - - refresh(); + replaceBody(); + serverStatusResultWrapper.addListener(new WeakChangeListener<>(listener)); + refreshIfNoStatus(); } public void replaceBody() { - HBox contentBox = new HBox(); - setMargin(contentBox, new Insets(20, 0, 0, 0)); + ServerStatusResult statusResult = serverStatusResultWrapper.get(); + ServerStatus status; + + if (statusResult == null) { + status = null; + } else { + status = statusResult.getIfSuccess(); + } Image serverIcon = iconedServer.iconImage; if (status != null) { serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); } + refreshSpinner.hideSpinner(); + if (statusResult != null) { + if (statusResult.exceptionIfFailure() != null) { + lblErrorMessage.setText(i18n("servers.manager.error.status")); + } else { + lblErrorMessage.setText(""); + } + } + + HBox contentBox = new HBox(); + setMargin(contentBox, new Insets(20, 0, 0, 0)); + + int iconScale = 1; ImageContainer imageView = new ImageContainer(64); imageView.setImage(serverIcon); @@ -165,35 +191,31 @@ public void replaceBody() { textPane.add(new Label(i18n("servers.manager.server.ip") + ":"), 0, rows); textPane.add(new Label(iconedServer.getIp()), 1, rows); - ServerStatus cachedServerStatus = status; - if (cachedServerStatus != null) { + if (status != null) { rows++; textPane.add(new Label(i18n("servers.manager.server.latency") + ":"), 0, rows); - textPane.add(new Label(String.format("%,dms", cachedServerStatus.networkLatency())), 1, rows); + textPane.add(new Label(String.format("%,dms", status.networkLatency())), 1, rows); rows++; textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); - textPane.add(new Label(String.format("%,d/%,d", cachedServerStatus.players().online(), cachedServerStatus.players().max())), 1, rows); + textPane.add(new Label(String.format("%,d/%,d", status.players().online(), status.players().max())), 1, rows); } rootLayout.setBody(contentBox); } + private void refreshIfNoStatus() { + if (serverStatusResultWrapper.get() == null) refresh(); + } + private void refresh() { - lblErrorMessage.setText(""); refreshSpinner.showSpinner(); + Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(iconedServer.getIp()) ).whenComplete(Schedulers.javafx(), (result, ignored) -> { - refreshSpinner.hideSpinner(); - - this.status = result.getIfSuccess(); - if (status == null) { - lblErrorMessage.setText(i18n("servers.manager.error.status")); - } - - replaceBody(); + serverStatusResultWrapper.set(result); }).start(); } From 0709a781810b8a4b662b61dbcf5e7875b4221650 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 27 Sep 2026 22:17:10 +0800 Subject: [PATCH 18/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20StatusPane=20?= =?UTF-8?q?=E4=B8=80=E5=A4=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index d4d726082f3..9db83ce1bb1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -210,6 +210,7 @@ private void refreshIfNoStatus() { } private void refresh() { + lblErrorMessage.setText(""); refreshSpinner.showSpinner(); Task.supplyAsync(Schedulers.io(), () -> From 58b6c0c674b63b2703522d1df939348b5dc43618 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 17:37:21 +0800 Subject: [PATCH 19/97] =?UTF-8?q?=E5=88=A0=E9=99=A4=20ServerStatus=20?= =?UTF-8?q?=E5=86=85=E5=AF=B9=20description=20=E7=9A=84=E5=B0=81=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/jackhuang/hmcl/server/ServerStatus.java | 6 +----- .../org/jackhuang/hmcl/server/ServerStatusGetter.java | 10 ++-------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index ec05dd45730..0eb0ca40718 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -28,7 +28,7 @@ public record ServerStatus( long networkLatency, @NotNull Version version, @NotNull Players players, - @NotNull Description description, + @NotNull JsonElement description, @Nullable String favicon, boolean enforcesSecureChat, @Nullable ModInfo modInfo @@ -54,10 +54,6 @@ public record Sample( } } - public record Description(@NotNull JsonElement description) { - - } - public record ModInfo( @NotNull String type, @NotNull List modList diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java index b02e1188e8c..73577bd0b72 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java @@ -25,6 +25,7 @@ import java.net.StandardSocketOptions; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Objects; import java.util.UUID; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -88,13 +89,6 @@ private static ServerStatus parseServerStatus(long networkLatency, JsonObject ro } } - ServerStatus.Description description; - if (rootStatus.get("description") != null) { - description = new ServerStatus.Description(rootStatus.get("description")); - } else { - description = new ServerStatus.Description(JsonNull.INSTANCE); - } - String favicon; if (rootStatus.get("favicon") != null) { String fetchedFavicon = rootStatus.get("favicon").getAsString(); @@ -139,7 +133,7 @@ private static ServerStatus parseServerStatus(long networkLatency, JsonObject ro networkLatency, version, players, - description, + Objects.requireNonNullElse(rootStatus.get("description"), JsonNull.INSTANCE), favicon, enforcesSecureChat, modInfo From 8c8d35e00e4e7b6db404eced3c804e0559a55001 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 17:52:35 +0800 Subject: [PATCH 20/97] =?UTF-8?q?=E9=99=90=E5=88=B6=20txt=20name=20?= =?UTF-8?q?=E5=8F=AF=E8=BE=93=E5=85=A5=E7=9A=84=E6=9C=80=E5=A4=A7=E9=95=BF?= =?UTF-8?q?=E5=BA=A6=E4=B8=BA=2032?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/server/EditServerPane.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index b73fccc0177..d9f4bc093a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -25,6 +25,7 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.control.TextFormatter; import javafx.scene.layout.*; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusGetter; @@ -102,6 +103,12 @@ private void initPaneContents() { body.add(label, 0, 0); txtServerName.setPromptText(i18n("servers.manager.server.name.def")); + txtServerName.setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().length() <= 32) { + return change; + } + return null; + })); body.add(txtServerName, 1, 0); if (reference != null) { From 728a0e1cb7a05925127a3d4532c860e0c969c4bb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 19:47:31 +0800 Subject: [PATCH 21/97] =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=A1=86=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E6=9B=B4=E5=A4=9A=E6=9C=89=E7=94=A8=E7=9A=84=E4=BF=A1?= =?UTF-8?q?=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerStatusPane.java | 102 +++++++++++---- HMCL/src/main/resources/assets/css/root.css | 60 +++++++++ .../resources/assets/lang/I18N.properties | 3 + .../resources/assets/lang/I18N_ar.properties | 3 + .../resources/assets/lang/I18N_de.properties | 3 + .../resources/assets/lang/I18N_es.properties | 3 + .../resources/assets/lang/I18N_ja.properties | 3 + .../resources/assets/lang/I18N_ru.properties | 3 + .../resources/assets/lang/I18N_uk.properties | 3 + .../assets/lang/I18N_zh_Hans.properties | 3 + .../assets/lang/I18N_zh_Hant.properties | 3 + .../util/MinecraftChatComponentUtils.java | 117 ++++++++++++++++++ 12 files changed, 282 insertions(+), 24 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 9db83ce1bb1..c18b6e77d24 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.instances.server; +import com.google.gson.JsonPrimitive; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; import javafx.beans.property.ReadOnlyObjectWrapper; @@ -29,6 +30,7 @@ import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.server.ServerStatus; @@ -41,7 +43,9 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; +import org.jackhuang.hmcl.util.MinecraftChatComponentUtils; +import java.util.List; import java.util.Objects; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -134,6 +138,28 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser refreshIfNoStatus(); } + private static VBox createStatCell(String titleText, String valueText, boolean showDivider) { + VBox cell = new VBox(3); + cell.getStyleClass().add("server-status-stat-cell"); + if (!showDivider) { + cell.getStyleClass().add("server-status-stat-cell-last"); + } + cell.setAlignment(Pos.CENTER_LEFT); + cell.setPadding(new Insets(10, 12, 10, 12)); + cell.setPrefWidth(140); + + Label title = new Label(titleText); + title.getStyleClass().add("server-status-stat-title"); + + Label value = new Label(valueText == null || valueText.isBlank() ? "—" : valueText); + value.getStyleClass().add("server-status-stat-value"); + value.setWrapText(true); + value.setMaxWidth(100); + + cell.getChildren().addAll(title, value); + return cell; + } + public void replaceBody() { ServerStatusResult statusResult = serverStatusResultWrapper.get(); ServerStatus status; @@ -158,21 +184,18 @@ public void replaceBody() { } } - HBox contentBox = new HBox(); - setMargin(contentBox, new Insets(20, 0, 0, 0)); + VBox root = new VBox(); + root.setSpacing(10); + HBox serverInfoBox = new HBox(); + VBox.setMargin(serverInfoBox, new Insets(10, 0, 0, 5)); - - int iconScale = 1; ImageContainer imageView = new ImageContainer(64); imageView.setImage(serverIcon); - imageView.setScaleX(iconScale); - imageView.setScaleY(iconScale); - StackPane canvasPane = new StackPane(imageView); - canvasPane.setPrefWidth(iconScale * 64); - canvasPane.setPrefHeight(iconScale * 64); - contentBox.getChildren().add(canvasPane); + canvasPane.setPrefWidth(64); + canvasPane.setPrefHeight(64); + serverInfoBox.getChildren().add(canvasPane); GridPane textPane = new GridPane(); textPane.setAlignment(Pos.CENTER_LEFT); @@ -180,29 +203,60 @@ public void replaceBody() { textPane.setHgap(10); textPane.setVgap(10); - contentBox.getChildren().add(textPane); + serverInfoBox.getChildren().add(textPane); HBox.setMargin(textPane, new Insets(0, 0, 0, 20)); - int rows = 0; - textPane.add(new Label(i18n("servers.manager.server.name") + ":"), 0, rows); - textPane.add(new Label(iconedServer.getName()), 1, rows); + Label label = new Label(i18n("servers.manager.server.name") + ":"); + textPane.add(label, 0, 0); + textPane.add(new Label(iconedServer.getName()), 1, 0); - rows++; - textPane.add(new Label(i18n("servers.manager.server.ip") + ":"), 0, rows); - textPane.add(new Label(iconedServer.getIp()), 1, rows); + textPane.add(new Label(i18n("servers.manager.server.ip") + ":"), 0, 1); + textPane.add(new Label(iconedServer.getIp()), 1, 1); + root.getChildren().add(serverInfoBox); if (status != null) { + VBox motdBox = new VBox(5); + motdBox.getStyleClass().add("server-status-motd-box"); + motdBox.setPadding(new Insets(10, 12, 10, 12)); + motdBox.setMaxWidth(620); + + Label motdTitle = new Label("MOTD"); + motdTitle.getStyleClass().add("server-status-motd-title"); + motdBox.getChildren().add(motdTitle); + + String motd = MinecraftChatComponentUtils.toPlainStringFromChatComponent(status.description()); + String[] motdLines = motd.split("\\r?\\n"); + for (int i = 0; i < Math.min(motdLines.length, 2); i++) { + String line = motdLines[i]; + if (line.length() > 80) { + line = line.substring(0, 77).trim() + "..."; + } + Label lineLabel = new Label(line); + lineLabel.getStyleClass().add("server-status-motd-line"); + lineLabel.setMaxWidth(560); + lineLabel.setWrapText(true); + motdBox.getChildren().add(lineLabel); + } + + root.getChildren().add(motdBox); + + HBox detailRow = new HBox(); + detailRow.getStyleClass().add("server-status-detail-row"); + detailRow.setSpacing(0); + detailRow.setMaxWidth(620); - rows++; - textPane.add(new Label(i18n("servers.manager.server.latency") + ":"), 0, rows); - textPane.add(new Label(String.format("%,dms", status.networkLatency())), 1, rows); + var cells = List.of( + createStatCell(i18n("servers.manager.server.type"), status.modInfo() != null ? status.modInfo().type() + "(" + status.modInfo().modList().size() + " mods)" : i18n("servers.manager.server.vanilla"), true), + createStatCell(i18n("servers.manager.server.players"), String.format("%,d/%,d", status.players().online(), status.players().max()), true), + createStatCell(i18n("servers.manager.server.version"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true), + createStatCell(i18n("servers.manager.server.latency"), String.format("%,dms", status.networkLatency()), false) + ); + detailRow.getChildren().addAll(cells); - rows++; - textPane.add(new Label(i18n("servers.manager.server.players") + ":"), 0, rows); - textPane.add(new Label(String.format("%,d/%,d", status.players().online(), status.players().max())), 1, rows); + root.getChildren().add(detailRow); } - rootLayout.setBody(contentBox); + rootLayout.setBody(root); } private void refreshIfNoStatus() { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 634c20a7248..eb2cd815675 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -582,6 +582,66 @@ -fx-padding: 0.7em 0.8em; } +/******************************************************************************* + * * + * Server Status Dialog * + * * + ******************************************************************************/ + +.server-status-motd-box { + -fx-background-color: -monet-surface-container; + -fx-background-radius: 10px; + -fx-border-color: -monet-outline-variant; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-max-width: 620px; +} + +.server-status-motd-title { + -fx-font-size: 12px; + -fx-font-weight: 700; + -fx-text-fill: -monet-on-surface; +} + +.server-status-motd-line { + -fx-font-size: 13px; + -fx-text-fill: -monet-on-surface; + -fx-wrap-text: true; + -fx-max-width: 560px; +} + +.server-status-detail-row { + -fx-background-color: -monet-surface-container-low; + -fx-background-radius: 10px; + -fx-border-color: -monet-outline-variant; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-max-width: 620px; +} + +.server-status-stat-cell { + -fx-border-color: -monet-outline-variant; + -fx-border-width: 0 1 0 0; +} + +.server-status-stat-cell-last { + -fx-border-width: 0; +} + +.server-status-stat-title { + -fx-font-size: 12px; + -fx-font-weight: 600; + -fx-text-fill: -monet-on-surface-variant; +} + +.server-status-stat-value { + -fx-font-size: 13px; + -fx-font-weight: 500; + -fx-text-fill: -monet-on-surface; + -fx-wrap-text: true; + -fx-max-width: 100px; +} + /******************************************************************************* * * * Microsoft Login Dialog * diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 402263e17f4..76fede9ce17 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1838,6 +1838,9 @@ servers.manager.add.head=Add Server servers.manager.server.name=Server Name servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=Server IP address +servers.manager.server.type=Server Type +servers.manager.server.version=Version +servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Current latency servers.manager.server.players=Online servers.manager.add.accept=Add diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index ad0909b5eb7..31796bec68a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1835,6 +1835,9 @@ servers.manager.add.head=إضافة الخادم servers.manager.server.name=اسم الخادم servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=عنوان IP الخادم +servers.manager.server.type=نوع الخادم +servers.manager.server.version=الإصدار +servers.manager.server.vanilla=الفانيلا servers.manager.server.latency=التأخير الحالي servers.manager.server.players=على الانترنت servers.manager.add.accept=إضافة diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 7b45e436a68..a25eb6bd69d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1837,6 +1837,9 @@ servers.manager.add.head=Server hinzufügen servers.manager.server.name=Name des Servers servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=IP-Adresse des Servers +servers.manager.server.type=Servertyp +servers.manager.server.version=Version +servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Aktuelle Latenz servers.manager.server.players=Online servers.manager.add.accept=Hinzufügen diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 98301e905b9..74ab88d7635 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1821,6 +1821,9 @@ servers.manager.add.head=Serveri lisamine servers.manager.server.name=Serveri nimi servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=Serveri IP-aadress +servers.manager.server.type=Tipo de servidor +servers.manager.server.version=Versión +servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Praegune viivitus servers.manager.server.players=Internetis servers.manager.add.accept=Lisa diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 95f7ab75b1a..5368e9530bd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1623,6 +1623,9 @@ servers.manager.add.head=サーバーの追加 servers.manager.server.name=サーバ名 servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=サーバipアドレス +servers.manager.server.type=サーバーの種類 +servers.manager.server.version=バージョン +servers.manager.server.vanilla=バニラ servers.manager.server.latency=現在の遅延 servers.manager.server.players=オンライン servers.manager.add.accept=追加 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index c0a972b89c7..608cc2dc136 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1755,6 +1755,9 @@ servers.manager.add.head=Добавить сервер servers.manager.server.name=Имя сервера servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=IP-адрес сервера +servers.manager.server.type=Тип сервера +servers.manager.server.version=Версия +servers.manager.server.vanilla=Ваниль servers.manager.server.latency=Текущая задержка servers.manager.server.players=Онлайн servers.manager.add.accept=Добавить diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index d18108490b8..163568b8d26 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1784,6 +1784,9 @@ servers.manager.add.head=Додати сервер servers.manager.server.name=Назва сервера servers.manager.server.name.def=Minecraft Server servers.manager.server.ip=IP-адреса сервера +servers.manager.server.type=Тип сервера +servers.manager.server.version=Версія +servers.manager.server.vanilla=Ваніль servers.manager.server.latency=Поточна затримка servers.manager.server.players=Онлайн servers.manager.add.accept=Додати 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 0fb3aabd954..710f834da77 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1645,6 +1645,9 @@ servers.manager.add.head=添加服务器 servers.manager.server.name=服务器名字 servers.manager.server.name.def=Minecraft 服务器 servers.manager.server.ip=服务器IP +servers.manager.server.type=服务器类型 +servers.manager.server.version=版本 +servers.manager.server.vanilla=原版 servers.manager.server.latency=当前延迟 servers.manager.server.players=在线人数 servers.manager.add.accept=添加 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 e4bdd2bb76d..3f9fa7d90eb 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1639,6 +1639,9 @@ servers.manager.add.head=新增伺服器 servers.manager.server.name=伺服器名稱 servers.manager.server.name.def=Minecraft 伺服器 servers.manager.server.ip=伺服器位址 +servers.manager.server.type=伺服器類型 +servers.manager.server.version=版本 +servers.manager.server.vanilla=原版 servers.manager.server.latency=當前延遲 servers.manager.server.players=線上人數 servers.manager.add.accept=新增 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java new file mode 100644 index 00000000000..9d1e34110db --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java @@ -0,0 +1,117 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +public final class MinecraftChatComponentUtils { + private MinecraftChatComponentUtils() { + + } + + public static String toPlainStringFromChatComponent(JsonElement chat) { + if (chat == null || chat.isJsonNull()) { + return ""; + } + + StringBuilder sb = new StringBuilder(); + extractText(chat, sb); + return sb.toString().replaceAll("§.?", ""); + } + + private static void extractText(JsonElement chat, StringBuilder sb) { + if (chat == null || chat.isJsonNull()) { + return; + } + + if (chat.isJsonPrimitive()) { + JsonPrimitive prim = chat.getAsJsonPrimitive(); + if (prim.isString()) { + sb.append(prim.getAsString()); + } else if (prim.isBoolean()) { + sb.append(prim.getAsBoolean()); + } else { + sb.append(prim.getAsNumber().toString()); + } + return; + } + + if (chat.isJsonArray()) { + for (JsonElement child : chat.getAsJsonArray()) { + extractText(child, sb); + } + return; + } + + if (chat.isJsonObject()) { + JsonObject obj = chat.getAsJsonObject(); + + if (obj.has("text")) { + JsonElement textEl = obj.get("text"); + if (textEl.isJsonPrimitive()) { + sb.append(textEl.getAsString()); + } + } + + if (obj.has("translate")) { + sb.append(obj.get("translate").getAsString()); + if (obj.has("with")) { + JsonArray withArr = obj.getAsJsonArray("with"); + for (JsonElement w : withArr) { + extractText(w, sb); + } + } + } + + if (obj.has("score")) { + JsonObject scoreObj = obj.getAsJsonObject("score"); + if (scoreObj.has("name")) sb.append(scoreObj.get("name").getAsString()); + if (scoreObj.has("objective")) sb.append(":").append(scoreObj.get("objective").getAsString()); + } + + if (obj.has("selector")) { + sb.append(obj.get("selector").getAsString()); + } + + if (obj.has("keybind")) { + sb.append(obj.get("keybind").getAsString()); + } + + if (obj.has("nbt")) { + sb.append(obj.get("nbt").getAsString()); + } + + if (obj.has("rawtext")) { + JsonArray rawArr = obj.getAsJsonArray("rawtext"); + for (JsonElement child : rawArr) { + extractText(child, sb); + } + } + + if (obj.has("extra")) { + JsonArray extraArr = obj.getAsJsonArray("extra"); + for (JsonElement child : extraArr) { + extractText(child, sb); + } + } + } + } +} From 3881aec00434ebd3f524535583b9c1b0e3067409 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 22:06:21 +0800 Subject: [PATCH 22/97] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=92=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=BF=BB=E8=AF=91=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/assets/lang/I18N.properties | 14 +++++++------- .../main/resources/assets/lang/I18N_ar.properties | 2 +- .../main/resources/assets/lang/I18N_de.properties | 2 +- .../main/resources/assets/lang/I18N_es.properties | 2 +- .../main/resources/assets/lang/I18N_ja.properties | 2 +- .../main/resources/assets/lang/I18N_ru.properties | 2 +- .../main/resources/assets/lang/I18N_uk.properties | 4 +++- .../resources/assets/lang/I18N_zh_Hans.properties | 11 +++++------ .../resources/assets/lang/I18N_zh_Hant.properties | 7 +++---- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 76fede9ce17..5c28a481835 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1825,20 +1825,20 @@ wizard.next=Next > servers.manage=Servers servers.show_all=Show All servers.show_hide=Show Hidden -servers.manage.copy.server.ip=Copy IP address -servers.manage.copy.server.ip.ok.toast=Server IP address has been copied to clipboard +servers.manage.copy.server.ip=Copy address +servers.manage.copy.server.ip.ok.toast=Server address copied to clipboard servers.manage.copy.to.instance=Copy to current instance servers.manage.delete=Delete server.delete=Delete this server server.tag.hide=Hidden server.tag.hold.current=Current -server.empty=No Server +server.empty=No Servers servers.manager.add=Add Server servers.manager.add.head=Add Server -servers.manager.server.name=Server Name +servers.manager.server.name=Server name servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=Server IP address -servers.manager.server.type=Server Type +servers.manager.server.ip=Server address +servers.manager.server.type=Server type servers.manager.server.version=Version servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Current latency @@ -1847,7 +1847,7 @@ servers.manager.add.accept=Add servers.manager.add.still=Still Add servers.dnd.add=Add to current instance servers.dnd.add.instanceempty=Add failed -servers.dnd.add.instanceempty.desc=There are no games available to add.\nYou can go to the download page to download a new game, or switch game folders in the "Instance List". +servers.dnd.add.instanceempty.desc=There are no games available to add.\nYou can go to the "Download" page to download a new game, or switch game directories in the "All Instances" page. servers.manager.edit=Edit Server servers.manager.edit.head=Edit Server servers.manager.edit.accept=Submit diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 31796bec68a..740c4036846 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1854,4 +1854,4 @@ servers.manager.status=حالة الخادم servers.manager.status.head=حالة الخادم servers.manager.status.outside.pinging=حالة الخادم (جارٍ الفحص...) servers.manager.status.outside.pong=حالة الخادم (%sms) -servers.manager.status.outside.error=حالة الخادم (فشل) \ No newline at end of file +servers.manager.status.outside.error=حالة الخادم (فشل) diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index a25eb6bd69d..d5c45a2443b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1856,4 +1856,4 @@ servers.manager.status=Serverstatus servers.manager.status.head=Serverstatus servers.manager.status.outside.pinging=Serverstatus (Pingt...) servers.manager.status.outside.pong=Serverstatus (%sms) -servers.manager.status.outside.error=Serverstatus (Fehlgeschlagen) \ No newline at end of file +servers.manager.status.outside.error=Serverstatus (Fehlgeschlagen) diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 74ab88d7635..5a489c3cfd1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1840,4 +1840,4 @@ servers.manager.status=Serveri seisund servers.manager.status.head=Serveri seisund servers.manager.status.outside.pinging=Serveri seisund (kontrollitakse...) servers.manager.status.outside.pong=Serveri seisund (%sms) -servers.manager.status.outside.error=Serveri seisund (ebaõnnestus) \ No newline at end of file +servers.manager.status.outside.error=Serveri seisund (ebaõnnestus) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 5368e9530bd..45004b72d4c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1642,4 +1642,4 @@ servers.manager.status=サーバの状態 servers.manager.status.head=サーバの状態 servers.manager.status.outside.pinging=サーバの状態 (Ping中...) servers.manager.status.outside.pong=サーバの状態 (%sms) -servers.manager.status.outside.error=サーバの状態 (失敗) \ No newline at end of file +servers.manager.status.outside.error=サーバの状態 (失敗) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 608cc2dc136..f7daad5261d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1774,4 +1774,4 @@ servers.manager.status=Статус сервера servers.manager.status.head=Статус сервера servers.manager.status.outside.pinging=Статус сервера (пинг...) servers.manager.status.outside.pong=Статус сервера (%sms) -servers.manager.status.outside.error=Статус сервера (не удалось) \ No newline at end of file +servers.manager.status.outside.error=Статус сервера (не удалось) diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 163568b8d26..878e2e57fc4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1775,6 +1775,8 @@ servers.show_all=Показати все servers.show_hide=Показати Приховане servers.manage.copy.server.ip=Копіювати IP-адресу servers.manage.copy.server.ip.ok.toast=IP-адреса сервера скопійована в буфер обміну +servers.manage.copy.to.instance=Копіювати до поточного випадку +servers.manage.delete=Вилучити server.delete=Видалити цей сервер server.tag.hide=Приховане server.tag.hold.current=ВласнийПоточний @@ -1803,4 +1805,4 @@ servers.manager.status=Статус сервера servers.manager.status.head=Статус сервера servers.manager.status.outside.pinging=Статус сервера (пінг...) servers.manager.status.outside.pong=Статус сервера (%sms) -servers.manager.status.outside.error=Статус сервера (помилка) \ No newline at end of file +servers.manager.status.outside.error=Статус сервера (помилка) 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 710f834da77..da71772cfe3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1639,12 +1639,11 @@ server.delete=删除此服务器 server.tag.hide=隐藏 server.tag.hold.current=当前实例 server.empty=没有服务器 - servers.manager.add=添加服务器 servers.manager.add.head=添加服务器 servers.manager.server.name=服务器名字 servers.manager.server.name.def=Minecraft 服务器 -servers.manager.server.ip=服务器IP +servers.manager.server.ip=服务器地址 servers.manager.server.type=服务器类型 servers.manager.server.version=版本 servers.manager.server.vanilla=原版 @@ -1654,7 +1653,7 @@ servers.manager.add.accept=添加 servers.manager.add.still=依然添加 servers.dnd.add=添加到当前实例 servers.dnd.add.instanceempty=添加失败 -servers.dnd.add.instanceempty.desc=没有可添加的游戏。\n你可以前往下载页面下载新游戏,或在“实例列表”中切换游戏文件夹。 +servers.dnd.add.instanceempty.desc=没有可添加的游戏。\n你可以前往“下载”页面下载新游戏,或在“实例列表”中切换游戏文件夹。 servers.manager.edit=编辑服务器 servers.manager.edit.head=编辑服务器 servers.manager.edit.accept=提交 @@ -1662,6 +1661,6 @@ servers.manager.edit.still=依然提交 servers.manager.error.status=无法获取目标服务器状态 servers.manager.status=服务器状态 servers.manager.status.head=服务器状态 -servers.manager.status.outside.pinging=服务器状态(Pinging...) -servers.manager.status.outside.pong=服务器状态(%sms) -servers.manager.status.outside.error=服务器状态(失败) \ No newline at end of file +servers.manager.status.outside.pinging=服务器状态 (Pinging...) +servers.manager.status.outside.pong=服务器状态 (%sms) +servers.manager.status.outside.error=服务器状态 (失败) 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 3f9fa7d90eb..1d9ebd466f0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1633,7 +1633,6 @@ server.delete=刪除此伺服器 server.tag.hide=隱藏 server.tag.hold.current=目前實例 server.empty=沒有伺服器 - servers.manager.add=新增伺服器 servers.manager.add.head=新增伺服器 servers.manager.server.name=伺服器名稱 @@ -1648,14 +1647,14 @@ servers.manager.add.accept=新增 servers.manager.add.still=依然新增 servers.dnd.add=新增到目前實例 servers.dnd.add.instanceempty=新增失敗 -servers.dnd.add.instanceempty.desc=沒有可新增的遊戲。\n你可以前往下載頁面下載新遊戲,或在「實例列表」中切換遊戲資料夾。 +servers.dnd.add.instanceempty.desc=沒有可新增的遊戲。\n你可以前往「下載」頁面下載新遊戲,或在「實例清單」中切換遊戲目錄。 servers.manager.edit=編輯伺服器 servers.manager.edit.head=編輯伺服器 servers.manager.edit.accept=提交 servers.manager.edit.still=仍然提交 -servers.manager.error.status=無法獲取目標伺服器狀態 +servers.manager.error.status=無法取得目標伺服器狀態 servers.manager.status=伺服器狀態 servers.manager.status.head=伺服器狀態 servers.manager.status.outside.pinging=伺服器狀態 (Pinging...) servers.manager.status.outside.pong=伺服器狀態 (%sms) -servers.manager.status.outside.error=伺服器狀態 (失敗) \ No newline at end of file +servers.manager.status.outside.error=伺服器狀態 (失敗) From c0bc41e6a281b8855095a716e0f2b9746702ba3f Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 22:19:41 +0800 Subject: [PATCH 23/97] =?UTF-8?q?=E8=BE=93=E5=87=BA=E7=A1=AC=E7=BC=96?= =?UTF-8?q?=E7=A0=81=E7=9A=84=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/server/ServerStatusPane.java | 4 ++-- HMCL/src/main/resources/assets/lang/I18N.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ar.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_de.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_es.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ja.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_ru.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_uk.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties | 2 ++ HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties | 2 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- 11 files changed, 21 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index c18b6e77d24..ce9497d32a8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -220,7 +220,7 @@ public void replaceBody() { motdBox.setPadding(new Insets(10, 12, 10, 12)); motdBox.setMaxWidth(620); - Label motdTitle = new Label("MOTD"); + Label motdTitle = new Label(i18n("servers.manager.server.motd")); motdTitle.getStyleClass().add("server-status-motd-title"); motdBox.getChildren().add(motdTitle); @@ -246,7 +246,7 @@ public void replaceBody() { detailRow.setMaxWidth(620); var cells = List.of( - createStatCell(i18n("servers.manager.server.type"), status.modInfo() != null ? status.modInfo().type() + "(" + status.modInfo().modList().size() + " mods)" : i18n("servers.manager.server.vanilla"), true), + createStatCell(i18n("servers.manager.server.type"), status.modInfo() != null ? i18n("servers.manager.server.modtypeinfo", status.modInfo().type(), status.modInfo().modList().size()) : i18n("servers.manager.server.vanilla"), true), createStatCell(i18n("servers.manager.server.players"), String.format("%,d/%,d", status.players().online(), status.players().max()), true), createStatCell(i18n("servers.manager.server.version"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true), createStatCell(i18n("servers.manager.server.latency"), String.format("%,dms", status.networkLatency()), false) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 5c28a481835..d694ef5162b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1843,6 +1843,8 @@ servers.manager.server.version=Version servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Current latency servers.manager.server.players=Online +servers.manager.server.modtypeinfo=%s (%,d mods) +servers.manager.server.motd=MOTD servers.manager.add.accept=Add servers.manager.add.still=Still Add servers.dnd.add=Add to current instance diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 740c4036846..200c0b61766 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1840,6 +1840,8 @@ servers.manager.server.version=الإصدار servers.manager.server.vanilla=الفانيلا servers.manager.server.latency=التأخير الحالي servers.manager.server.players=على الانترنت +servers.manager.server.modtypeinfo=%s (%,d تعديلات) +servers.manager.server.motd=MOTD servers.manager.add.accept=إضافة servers.manager.add.still=لا تزال إضافة servers.dnd.add=إضافة إلى هذه الحالة الحالية diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index d5c45a2443b..5a447bcf435 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1842,6 +1842,8 @@ servers.manager.server.version=Version servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Aktuelle Latenz servers.manager.server.players=Online +servers.manager.server.modtypeinfo=%s (%,d Mods) +servers.manager.server.motd=MOTD servers.manager.add.accept=Hinzufügen servers.manager.add.still=Noch Hinzufügen servers.dnd.add=Zur aktuellen Instanz hinzufügen diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 5a489c3cfd1..603ce5c7f97 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1826,6 +1826,8 @@ servers.manager.server.version=Versión servers.manager.server.vanilla=Vanilla servers.manager.server.latency=Praegune viivitus servers.manager.server.players=Internetis +servers.manager.server.modtypeinfo=%s (%,d mods) +servers.manager.server.motd=MOTD servers.manager.add.accept=Lisa servers.manager.add.still=Lisa ikkagi servers.dnd.add=Añadir a la instancia actual diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 45004b72d4c..711f14e388f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1628,6 +1628,8 @@ servers.manager.server.version=バージョン servers.manager.server.vanilla=バニラ servers.manager.server.latency=現在の遅延 servers.manager.server.players=オンライン +servers.manager.server.modtypeinfo=%s (%,d mods) +servers.manager.server.motd=MOTD servers.manager.add.accept=追加 servers.manager.add.still=追加 servers.dnd.add=現在のインスタンスに追加 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index f7daad5261d..697de846793 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1760,6 +1760,8 @@ servers.manager.server.version=Версия servers.manager.server.vanilla=Ваниль servers.manager.server.latency=Текущая задержка servers.manager.server.players=Онлайн +servers.manager.server.modtypeinfo=%s (%,d модов) +servers.manager.server.motd=MOTD servers.manager.add.accept=Добавить servers.manager.add.still=Добавить servers.dnd.add=Добавить в текущий экземпляр diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 878e2e57fc4..ba6d09a5be6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1791,6 +1791,8 @@ servers.manager.server.version=Версія servers.manager.server.vanilla=Ваніль servers.manager.server.latency=Поточна затримка servers.manager.server.players=Онлайн +servers.manager.server.modtypeinfo=%s (%,d модів) +servers.manager.server.motd=MOTD servers.manager.add.accept=Додати servers.manager.add.still=Додати servers.dnd.add=Додати до поточного екземпляра 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 da71772cfe3..260f7de2c52 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1649,6 +1649,8 @@ servers.manager.server.version=版本 servers.manager.server.vanilla=原版 servers.manager.server.latency=当前延迟 servers.manager.server.players=在线人数 +servers.manager.server.modtypeinfo=%s(%,d mods) +servers.manager.server.motd=MOTD servers.manager.add.accept=添加 servers.manager.add.still=依然添加 servers.dnd.add=添加到当前实例 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 1d9ebd466f0..b3a2aebb06d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1643,6 +1643,8 @@ servers.manager.server.version=版本 servers.manager.server.vanilla=原版 servers.manager.server.latency=當前延遲 servers.manager.server.players=線上人數 +servers.manager.server.modtypeinfo=%s(%,d 個模組) +servers.manager.server.motd=MOTD servers.manager.add.accept=新增 servers.manager.add.still=依然新增 servers.dnd.add=新增到目前實例 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8b827a4527d..ffeb9d29282 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.1-bin.zip distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a networkTimeout=10000 retries=0 From 2a56e26375dfa474dc32fa6e2b916f2910f3a7bd Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 22:24:37 +0800 Subject: [PATCH 24/97] update I18N_zh_Hant.properties --- HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b3a2aebb06d..5604941f194 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1643,7 +1643,7 @@ servers.manager.server.version=版本 servers.manager.server.vanilla=原版 servers.manager.server.latency=當前延遲 servers.manager.server.players=線上人數 -servers.manager.server.modtypeinfo=%s(%,d 個模組) +servers.manager.server.modtypeinfo=%s(%,d 個模組) servers.manager.server.motd=MOTD servers.manager.add.accept=新增 servers.manager.add.still=依然新增 From 9ca3c2b7e88348c507868375123832f7c30faa8d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 22:26:19 +0800 Subject: [PATCH 25/97] =?UTF-8?q?=E8=BF=98=E5=8E=9F=20distributionUrl?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ffeb9d29282..8b827a4527d 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-9.7.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip distributionSha256Sum=acd53f1edaf02f1a8ff99879f8a34b302661a057d9b063ae9e35b552f804d20a networkTimeout=10000 retries=0 From dfcf2ac8acbd392a763518b76fb36ebeb7a29b6a Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 28 Sep 2026 23:09:24 +0800 Subject: [PATCH 26/97] =?UTF-8?q?=E9=87=8D=E6=96=B0=E6=8E=92=E5=88=97?= =?UTF-8?q?=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/GameInstancePage.java | 2 +- .../ui/instances/server/EditServerPane.java | 22 +++--- .../ui/instances/server/ServerListPage.java | 24 +++--- .../ui/instances/server/ServerStatusPane.java | 22 +++--- .../resources/assets/lang/I18N.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_ar.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_de.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_es.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_ja.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_lzh.properties | 38 +++++++++ .../resources/assets/lang/I18N_ru.properties | 77 +++++++++---------- .../resources/assets/lang/I18N_uk.properties | 77 +++++++++---------- .../assets/lang/I18N_zh_Hans.properties | 77 +++++++++---------- .../assets/lang/I18N_zh_Hant.properties | 77 +++++++++---------- 14 files changed, 415 insertions(+), 386 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 7446e129b18..8f53460b774 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -369,7 +369,7 @@ protected Skin(GameInstancePage control) { .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL) - .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("servers.manage"), SVG.DRESSER); + .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("server.manage"), SVG.DRESSER); VBox.setVgrow(sideBar, Priority.ALWAYS); PopupMenu browseList = new PopupMenu(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index d9f4bc093a3..cf7eb109fa6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -76,8 +76,8 @@ private void initPaneContents() { JFXDialogLayout rootLayout = new JFXDialogLayout(); getChildren().setAll(rootLayout); rootLayout.setHeading(new Label(switch (type) { - case EDIT -> i18n("servers.manager.edit.head"); - case ADD -> i18n("servers.manager.add.head"); + case EDIT -> i18n("server.manage.edit.head"); + case ADD -> i18n("server.manage.add.head"); })); VBox bodyVbox = new VBox(); @@ -98,11 +98,11 @@ private void initPaneContents() { // server name { Label label = new Label(); - label.setText(i18n("servers.manager.server.name")); + label.setText(i18n("server.name")); GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 0); - txtServerName.setPromptText(i18n("servers.manager.server.name.def")); + txtServerName.setPromptText(i18n("server.name.def")); txtServerName.setTextFormatter(new TextFormatter<>(change -> { if (change.getControlNewText().length() <= 32) { return change; @@ -119,7 +119,7 @@ private void initPaneContents() { // server ip { Label label = new Label(); - label.setText(i18n("servers.manager.server.ip")); + label.setText(i18n("server.ip")); GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); @@ -136,8 +136,8 @@ private void initPaneContents() { rootLayout.setBody(bodyVbox); btnAccept.setText(switch (type) { - case EDIT -> i18n("servers.manager.edit.accept"); - case ADD -> i18n("servers.manager.add.accept"); + case EDIT -> i18n("server.manage.edit.accept"); + case ADD -> i18n("server.manage.add.accept"); }); btnAccept.getStyleClass().add("dialog-accept"); btnAccept.setOnAction(e -> onAdd()); @@ -175,7 +175,7 @@ private void onAdd() { String serverIP = txtServerIP.getText(); if (serverIP == null) return; if (txtServerName.getText() == null || txtServerName.getText().isEmpty()) { - serverName = i18n("servers.manager.server.name.def"); + serverName = i18n("server.name.def"); } else { serverName = txtServerName.getText(); } @@ -192,8 +192,8 @@ private void onAdd() { if (status == null) { spinner.hideSpinner(); btnAccept.setText(switch (type) { - case EDIT -> i18n("servers.manager.edit.still"); - case ADD -> i18n("servers.manager.add.still"); + case EDIT -> i18n("server.manage.edit.still"); + case ADD -> i18n("server.manage.add.still"); }); btnAccept.setOnAction(e -> { fireEvent(new DialogCloseEvent()); @@ -206,7 +206,7 @@ private void onAdd() { btnCancel.setOnAction(e -> { initPaneContents(); }); - lblErrorMessage.setText(i18n("servers.manager.error.status")); + lblErrorMessage.setText(i18n("server.manage.status.error")); } else { fireEvent(new DialogCloseEvent()); if (reference != null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 6503ec11d36..4c1303498bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -124,7 +124,7 @@ public void editServer(ServerHolder holder) { } public void copyServerIp(ServerHolder holder) { - FXUtils.copyText(holder.server.getIp(), i18n("servers.manage.copy.server.ip.ok.toast")); + FXUtils.copyText(holder.server.getIp(), i18n("server.manage.copy.server.ip.ok.toast")); } public void delete(ServerHolder holder) { @@ -314,7 +314,7 @@ public ServerListCell(ServerListPage page) { JFXButton editBtn = FXUtils.newToggleButton4(SVG.EDIT); right.getChildren().add(editBtn); - FXUtils.installFastTooltip(editBtn, i18n("servers.manager.edit")); + FXUtils.installFastTooltip(editBtn, i18n("server.manage.edit")); editBtn.setOnAction(event -> { ServerHolder holder = getItem(); if (holder != null) @@ -409,16 +409,16 @@ protected void updateItem(ServerHolder holder, boolean empty) { private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { if (result == null) { serverNetworkLatencyPane.ping(); - statusBtnTooltip.setText(i18n("servers.manager.status.outside.pinging")); + statusBtnTooltip.setText(i18n("server.manage.status.outside.pinging")); return; } ServerStatus serverStatus = result.getIfSuccess(); if (serverStatus == null) { serverNetworkLatencyPane.error(); - statusBtnTooltip.setText(i18n("servers.manager.status.outside.error")); + statusBtnTooltip.setText(i18n("server.manage.status.outside.error")); } else { serverNetworkLatencyPane.pong(serverStatus.networkLatency()); - statusBtnTooltip.setText(i18n("servers.manager.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); + statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); // update latest server icon serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { @@ -442,12 +442,12 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); - IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); + IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("server.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); popupMenu.getContent().addAll( - new IconedMenuItem(SVG.EDIT, i18n("servers.manager.edit"), () -> + new IconedMenuItem(SVG.EDIT, i18n("server.manage.edit"), () -> page.editServer(holder), popup ), - new IconedMenuItem(SVG.SERVER_SIGNAL_FULL, i18n("servers.manager.status"), () -> + new IconedMenuItem(SVG.SERVER_SIGNAL_FULL, i18n("server.manage.status"), () -> page.showServerStatus(holder), popup ), new MenuSeparator(), @@ -458,7 +458,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos page.generateLaunchScript(holder), popup ), new MenuSeparator(), - new IconedMenuItem(SVG.CONTENT_COPY, i18n("servers.manage.copy.server.ip"), () -> + new IconedMenuItem(SVG.CONTENT_COPY, i18n("server.manage.copy.server.ip"), () -> page.copyServerIp(holder), popup ), new MenuSeparator(), @@ -565,17 +565,17 @@ private final class ServerListPageSkin extends ToolbarListPageSkin initializeToolbar(ServerListPage skinnable) { - JFXCheckBox chkShowAll = new JFXCheckBox(i18n("servers.show_all")); + JFXCheckBox chkShowAll = new JFXCheckBox(i18n("server.manage.show_all")); chkShowAll.selectedProperty().bindBidirectional(skinnable.showAll); - JFXCheckBox chkShowHide = new JFXCheckBox(i18n("servers.show_hide")); + JFXCheckBox chkShowHide = new JFXCheckBox(i18n("server.manage.show_hide")); chkShowHide.selectedProperty().bindBidirectional(skinnable.showHide); return Arrays.asList( chkShowAll, chkShowHide, createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("servers.manager.add"), SVG.ADD, skinnable::addServer) + createToolbarButton2(i18n("server.manage.add"), SVG.ADD, skinnable::addServer) ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index ce9497d32a8..b2d286d2dd6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -71,7 +71,7 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser getStyleClass().add("skin-pane"); getChildren().setAll(rootLayout); - rootLayout.setHeading(new Label(i18n("servers.manager.status.head"))); + rootLayout.setHeading(new Label(i18n("server.manage.status.head"))); { JFXButton refreshBtn = new JFXButton(i18n("button.refresh")); @@ -87,7 +87,7 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser if (fromDnD) { // add to current instance - JFXButton addToInstallBtn = new JFXButton(i18n("servers.dnd.add")); + JFXButton addToInstallBtn = new JFXButton(i18n("server.dnd.add")); addToInstallBtn.getStyleClass().add("dialog-accept"); addToInstallBtn.setOnAction(ignored0 -> { fireEvent(new DialogCloseEvent()); @@ -99,7 +99,7 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser gotoDownload.getStyleClass().add("dialog-accept"); gotoDownload.setOnAction(ignored1 -> Controllers.navigate(Controllers.getDownloadPage())); - Controllers.confirmAction(i18n("servers.dnd.add.instanceempty.desc"), i18n("servers.dnd.add.instanceempty"), + Controllers.confirmAction(i18n("server.dnd.add.instanceempty.desc"), i18n("server.dnd.add.instanceempty"), MessageDialogPane.MessageType.ERROR, gotoDownload, null); @@ -178,7 +178,7 @@ public void replaceBody() { refreshSpinner.hideSpinner(); if (statusResult != null) { if (statusResult.exceptionIfFailure() != null) { - lblErrorMessage.setText(i18n("servers.manager.error.status")); + lblErrorMessage.setText(i18n("server.manage.status.error")); } else { lblErrorMessage.setText(""); } @@ -206,11 +206,11 @@ public void replaceBody() { serverInfoBox.getChildren().add(textPane); HBox.setMargin(textPane, new Insets(0, 0, 0, 20)); - Label label = new Label(i18n("servers.manager.server.name") + ":"); + Label label = new Label(i18n("server.name") + ":"); textPane.add(label, 0, 0); textPane.add(new Label(iconedServer.getName()), 1, 0); - textPane.add(new Label(i18n("servers.manager.server.ip") + ":"), 0, 1); + textPane.add(new Label(i18n("server.ip") + ":"), 0, 1); textPane.add(new Label(iconedServer.getIp()), 1, 1); root.getChildren().add(serverInfoBox); @@ -220,7 +220,7 @@ public void replaceBody() { motdBox.setPadding(new Insets(10, 12, 10, 12)); motdBox.setMaxWidth(620); - Label motdTitle = new Label(i18n("servers.manager.server.motd")); + Label motdTitle = new Label(i18n("server.motd")); motdTitle.getStyleClass().add("server-status-motd-title"); motdBox.getChildren().add(motdTitle); @@ -246,10 +246,10 @@ public void replaceBody() { detailRow.setMaxWidth(620); var cells = List.of( - createStatCell(i18n("servers.manager.server.type"), status.modInfo() != null ? i18n("servers.manager.server.modtypeinfo", status.modInfo().type(), status.modInfo().modList().size()) : i18n("servers.manager.server.vanilla"), true), - createStatCell(i18n("servers.manager.server.players"), String.format("%,d/%,d", status.players().online(), status.players().max()), true), - createStatCell(i18n("servers.manager.server.version"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true), - createStatCell(i18n("servers.manager.server.latency"), String.format("%,dms", status.networkLatency()), false) + createStatCell(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), true), + createStatCell(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), true), + createStatCell(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true), + createStatCell(i18n("server.latency"), String.format("%,dms", status.networkLatency()), false) ); detailRow.getChildren().addAll(cells); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index d694ef5162b..68f3fa53328 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1375,6 +1375,44 @@ selector.choose=Choose selector.choose_file=Choose file selector.choose_directory=Choose directory selector.custom=Custom +server.delete=Delete this server +server.dnd.add=Add to current instance +server.dnd.add.instanceempty=Add failed +server.dnd.add.instanceempty.desc=There are no games available to add.\nYou can go to the "Download" page to download a new game, or switch game directories in the "All Instances" page. +server.empty=No Servers +server.ip=Server address +server.latency=Current latency +server.manage=Servers +server.manage.add=Add Server +server.manage.add.accept=Add +server.manage.add.head=Add Server +server.manage.add.still=Still Add +server.manage.copy.server.ip=Copy address +server.manage.copy.server.ip.ok.toast=Server address copied to clipboard +server.manage.copy.to.instance=Copy to current instance +server.manage.delete=Delete +server.manage.show_all=Show All +server.manage.show_hide=Show Hidden +server.manage.edit=Edit Server +server.manage.edit.accept=Submit +server.manage.edit.head=Edit Server +server.manage.edit.still=Still Submit +server.manage.status=Server Status +server.manage.status.error=Unable to fetch the current server status +server.manage.status.head=Server Status +server.manage.status.outside.error=Server Status (Failed) +server.manage.status.outside.pinging=Server Status (Pinging...) +server.manage.status.outside.pong=Server Status (%sms) +server.motd=MOTD +server.name=Server name +server.name.def=Minecraft Server +server.onlineplayers=Online +server.playversion=Version +server.tag.hide=Hidden +server.tag.hold.current=Current +server.type=Server type +server.type.mod=%s (%,d mods) +server.type.vanilla=Vanilla settings=Settings @@ -1821,42 +1859,3 @@ wizard.prev=< Prev wizard.failed=Failed wizard.finish=Finish wizard.next=Next > - -servers.manage=Servers -servers.show_all=Show All -servers.show_hide=Show Hidden -servers.manage.copy.server.ip=Copy address -servers.manage.copy.server.ip.ok.toast=Server address copied to clipboard -servers.manage.copy.to.instance=Copy to current instance -servers.manage.delete=Delete -server.delete=Delete this server -server.tag.hide=Hidden -server.tag.hold.current=Current -server.empty=No Servers -servers.manager.add=Add Server -servers.manager.add.head=Add Server -servers.manager.server.name=Server name -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=Server address -servers.manager.server.type=Server type -servers.manager.server.version=Version -servers.manager.server.vanilla=Vanilla -servers.manager.server.latency=Current latency -servers.manager.server.players=Online -servers.manager.server.modtypeinfo=%s (%,d mods) -servers.manager.server.motd=MOTD -servers.manager.add.accept=Add -servers.manager.add.still=Still Add -servers.dnd.add=Add to current instance -servers.dnd.add.instanceempty=Add failed -servers.dnd.add.instanceempty.desc=There are no games available to add.\nYou can go to the "Download" page to download a new game, or switch game directories in the "All Instances" page. -servers.manager.edit=Edit Server -servers.manager.edit.head=Edit Server -servers.manager.edit.accept=Submit -servers.manager.edit.still=Still Submit -servers.manager.error.status=Unable to fetch the current server status -servers.manager.status=Server Status -servers.manager.status.head=Server Status -servers.manager.status.outside.pinging=Server Status (Pinging...) -servers.manager.status.outside.pong=Server Status (%sms) -servers.manager.status.outside.error=Server Status (Failed) \ No newline at end of file diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 200c0b61766..05d9992008a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1375,6 +1375,44 @@ selector.choose=اختر selector.choose_file=اختر ملفاً # selector.choose_directory= selector.custom=مخصص +server.delete=حذف هذا الخادم +server.dnd.add=إضافة إلى هذه الحالة الحالية +server.dnd.add.instanceempty=فشل الإضافة +server.dnd.add.instanceempty.desc=لا توجد ألعاب متاحة للإضافة.\nيمكنك الذهاب إلى صفحة التنزيل لتنزيل لعبة جديدة، أو تبديل مجلدات الألعاب في قائمة "المثيلات". +# server.empty= +server.ip=عنوان IP الخادم +server.latency=التأخير الحالي +server.manage=خوادم +server.manage.add=إضافة الخادم +server.manage.add.accept=إضافة +server.manage.add.head=إضافة الخادم +server.manage.add.still=لا تزال إضافة +server.manage.copy.server.ip=نسخ عنوان IP +server.manage.copy.server.ip.ok.toast=تم نسخ عنوان خادم IP إلى الحافظة +server.manage.copy.to.instance=نسخ إلى هذه الحالة +server.manage.delete=حذف +server.manage.show_all=عرض الكل +server.manage.show_hide=إظهار الإخفاء +server.manage.edit=تعديل الخادم +server.manage.edit.accept=إرسال +server.manage.edit.head=تعديل الخادم +server.manage.edit.still=الإرسال على أي حال +server.manage.status=حالة الخادم +server.manage.status.error=غير قادر على الحصول على حالة الخادم الحالية +server.manage.status.head=حالة الخادم +server.manage.status.outside.error=حالة الخادم (فشل) +server.manage.status.outside.pinging=حالة الخادم (جارٍ الفحص...) +server.manage.status.outside.pong=حالة الخادم (%sms) +server.motd=MOTD +server.name=اسم الخادم +server.name.def=Minecraft Server +server.onlineplayers=على الانترنت +server.playversion=الإصدار +server.tag.hide=مخفي +server.tag.hold.current=المحمل +server.type=نوع الخادم +server.type.mod=%s (%,d تعديلات) +server.type.vanilla=الفانيلا settings=الإعدادات @@ -1818,42 +1856,3 @@ wizard.prev=< السابق wizard.failed=فشل wizard.finish=إنهاء wizard.next=التالي > - -servers.manage=خوادم -servers.show_all=عرض الكل -servers.show_hide=إظهار الإخفاء -servers.manage.copy.server.ip=نسخ عنوان IP -servers.manage.copy.server.ip.ok.toast=تم نسخ عنوان خادم IP إلى الحافظة -servers.manage.copy.to.instance=نسخ إلى هذه الحالة -servers.manage.delete=حذف -server.delete=حذف هذا الخادم -server.tag.hide=مخفي -server.tag.hold.current=المحمل -# server.empty= -servers.manager.add=إضافة الخادم -servers.manager.add.head=إضافة الخادم -servers.manager.server.name=اسم الخادم -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=عنوان IP الخادم -servers.manager.server.type=نوع الخادم -servers.manager.server.version=الإصدار -servers.manager.server.vanilla=الفانيلا -servers.manager.server.latency=التأخير الحالي -servers.manager.server.players=على الانترنت -servers.manager.server.modtypeinfo=%s (%,d تعديلات) -servers.manager.server.motd=MOTD -servers.manager.add.accept=إضافة -servers.manager.add.still=لا تزال إضافة -servers.dnd.add=إضافة إلى هذه الحالة الحالية -servers.dnd.add.instanceempty=فشل الإضافة -servers.dnd.add.instanceempty.desc=لا توجد ألعاب متاحة للإضافة.\nيمكنك الذهاب إلى صفحة التنزيل لتنزيل لعبة جديدة، أو تبديل مجلدات الألعاب في قائمة "المثيلات". -servers.manager.edit=تعديل الخادم -servers.manager.edit.head=تعديل الخادم -servers.manager.edit.accept=إرسال -servers.manager.edit.still=الإرسال على أي حال -servers.manager.error.status=غير قادر على الحصول على حالة الخادم الحالية -servers.manager.status=حالة الخادم -servers.manager.status.head=حالة الخادم -servers.manager.status.outside.pinging=حالة الخادم (جارٍ الفحص...) -servers.manager.status.outside.pong=حالة الخادم (%sms) -servers.manager.status.outside.error=حالة الخادم (فشل) diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 5a447bcf435..b1fef703a9f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1374,6 +1374,44 @@ selector.choose=Auswählen selector.choose_file=Datei auswählen selector.choose_directory=Verzeichnis auswählen selector.custom=Benutzerdefiniert +server.delete=Diesen Server löschen +server.dnd.add=Zur aktuellen Instanz hinzufügen +server.dnd.add.instanceempty=Hinzufügen fehlgeschlagen +server.dnd.add.instanceempty.desc=Es gibt keine Spiele zum Hinzufügen.\nDu kannst die Downloadseite besuchen, um ein neues Spiel herunterzuladen, oder den Spielordner in der "Instanzliste" wechseln. +server.empty=Kein Server +server.ip=IP-Adresse des Servers +server.latency=Aktuelle Latenz +server.manage=Servers +server.manage.add=Server hinzufügen +server.manage.add.accept=Hinzufügen +server.manage.add.head=Server hinzufügen +server.manage.add.still=Noch Hinzufügen +server.manage.copy.server.ip=IP adresse kopieren +server.manage.copy.server.ip.ok.toast=Die IP addresse des Servers wurde in die Zwischenablage kopiert +server.manage.copy.to.instance=Auf die aktuelle Instanz kopieren +server.manage.delete=Löschen +server.manage.show_all=Alle anzeigen +server.manage.show_hide=Versteckt anzeigen +server.manage.edit=Server bearbeiten +server.manage.edit.accept=Übermitteln +server.manage.edit.head=Server bearbeiten +server.manage.edit.still=Trotzdem Übermitteln +server.manage.status=Serverstatus +server.manage.status.error=Der aktuelle Serverstatus kann nicht abgerufen werden +server.manage.status.head=Serverstatus +server.manage.status.outside.error=Serverstatus (Fehlgeschlagen) +server.manage.status.outside.pinging=Serverstatus (Pingt...) +server.manage.status.outside.pong=Serverstatus (%sms) +server.motd=MOTD +server.name=Name des Servers +server.name.def=Minecraft Server +server.onlineplayers=Online +server.playversion=Version +server.tag.hide=Versteckt +server.tag.hold.current=Aktuell +server.type=Servertyp +server.type.mod=%s (%,d Mods) +server.type.vanilla=Vanilla settings=Einstellungen @@ -1820,42 +1858,3 @@ wizard.prev=< Zurück wizard.failed=Fehlgeschlagen wizard.finish=Fertigstellen wizard.next=Weiter > - -servers.manage=Servers -servers.show_all=Alle anzeigen -servers.show_hide=Versteckt anzeigen -servers.manage.copy.server.ip=IP adresse kopieren -servers.manage.copy.server.ip.ok.toast=Die IP addresse des Servers wurde in die Zwischenablage kopiert -servers.manage.copy.to.instance=Auf die aktuelle Instanz kopieren -servers.manage.delete=Löschen -server.delete=Diesen Server löschen -server.tag.hide=Versteckt -server.tag.hold.current=Aktuell -server.empty=Kein Server -servers.manager.add=Server hinzufügen -servers.manager.add.head=Server hinzufügen -servers.manager.server.name=Name des Servers -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=IP-Adresse des Servers -servers.manager.server.type=Servertyp -servers.manager.server.version=Version -servers.manager.server.vanilla=Vanilla -servers.manager.server.latency=Aktuelle Latenz -servers.manager.server.players=Online -servers.manager.server.modtypeinfo=%s (%,d Mods) -servers.manager.server.motd=MOTD -servers.manager.add.accept=Hinzufügen -servers.manager.add.still=Noch Hinzufügen -servers.dnd.add=Zur aktuellen Instanz hinzufügen -servers.dnd.add.instanceempty=Hinzufügen fehlgeschlagen -servers.dnd.add.instanceempty.desc=Es gibt keine Spiele zum Hinzufügen.\nDu kannst die Downloadseite besuchen, um ein neues Spiel herunterzuladen, oder den Spielordner in der "Instanzliste" wechseln. -servers.manager.edit=Server bearbeiten -servers.manager.edit.head=Server bearbeiten -servers.manager.edit.accept=Übermitteln -servers.manager.edit.still=Trotzdem Übermitteln -servers.manager.error.status=Der aktuelle Serverstatus kann nicht abgerufen werden -servers.manager.status=Serverstatus -servers.manager.status.head=Serverstatus -servers.manager.status.outside.pinging=Serverstatus (Pingt...) -servers.manager.status.outside.pong=Serverstatus (%sms) -servers.manager.status.outside.error=Serverstatus (Fehlgeschlagen) diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 603ce5c7f97..c3c092590b0 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1367,6 +1367,44 @@ selector.choose=Elegir selector.choose_file=Elegir archivo # selector.choose_directory= selector.custom=Personalizar +server.delete=Eliminar este servidor +server.dnd.add=Añadir a la instancia actual +server.dnd.add.instanceempty=Error al añadir +server.dnd.add.instanceempty.desc=No hay juegos disponibles para añadir.\nPuedes ir a la página de descargas para descargar un juego nuevo o cambiar la carpeta del juego en la "Lista de instancias". +# server.empty= +server.ip=Serveri IP-aadress +server.latency=Praegune viivitus +server.manage=Servidores +server.manage.add=Serveri lisamine +server.manage.add.accept=Lisa +server.manage.add.head=Serveri lisamine +server.manage.add.still=Lisa ikkagi +server.manage.copy.server.ip=Copiar dirección IP +server.manage.copy.server.ip.ok.toast=Se ha copiado la dirección IP del servidor al portapapeles +server.manage.copy.to.instance=Copiar a esta instancia +server.manage.delete=Eliminar +server.manage.show_all=Mostrar todo +server.manage.show_hide=Mostrar Oculto +server.manage.edit=Editar servidor +server.manage.edit.accept=Enviar +server.manage.edit.head=Editar servidor +server.manage.edit.still=Enviar igualmente +server.manage.status=Serveri seisund +server.manage.status.error=Serveri praeguse seisundi leidmine nurjus +server.manage.status.head=Serveri seisund +server.manage.status.outside.error=Serveri seisund (ebaõnnestus) +server.manage.status.outside.pinging=Serveri seisund (kontrollitakse...) +server.manage.status.outside.pong=Serveri seisund (%sms) +server.motd=MOTD +server.name=Serveri nimi +server.name.def=Minecraft Server +server.onlineplayers=Internetis +server.playversion=Versión +server.tag.hide=Oculto +server.tag.hold.current=PoseídoActual +server.type=Tipo de servidor +server.type.mod=%s (%,d mods) +server.type.vanilla=Vanilla settings=Configuración @@ -1804,42 +1842,3 @@ wizard.prev=< Previo wizard.failed=Falló wizard.finish=Finalizar wizard.next=Siguiente > - -servers.manage=Servidores -servers.show_all=Mostrar todo -servers.show_hide=Mostrar Oculto -servers.manage.copy.server.ip=Copiar dirección IP -servers.manage.copy.server.ip.ok.toast=Se ha copiado la dirección IP del servidor al portapapeles -servers.manage.copy.to.instance=Copiar a esta instancia -servers.manage.delete=Eliminar -server.delete=Eliminar este servidor -server.tag.hide=Oculto -server.tag.hold.current=PoseídoActual -# server.empty= -servers.manager.add=Serveri lisamine -servers.manager.add.head=Serveri lisamine -servers.manager.server.name=Serveri nimi -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=Serveri IP-aadress -servers.manager.server.type=Tipo de servidor -servers.manager.server.version=Versión -servers.manager.server.vanilla=Vanilla -servers.manager.server.latency=Praegune viivitus -servers.manager.server.players=Internetis -servers.manager.server.modtypeinfo=%s (%,d mods) -servers.manager.server.motd=MOTD -servers.manager.add.accept=Lisa -servers.manager.add.still=Lisa ikkagi -servers.dnd.add=Añadir a la instancia actual -servers.dnd.add.instanceempty=Error al añadir -servers.dnd.add.instanceempty.desc=No hay juegos disponibles para añadir.\nPuedes ir a la página de descargas para descargar un juego nuevo o cambiar la carpeta del juego en la "Lista de instancias". -servers.manager.edit=Editar servidor -servers.manager.edit.head=Editar servidor -servers.manager.edit.accept=Enviar -servers.manager.edit.still=Enviar igualmente -servers.manager.error.status=Serveri praeguse seisundi leidmine nurjus -servers.manager.status=Serveri seisund -servers.manager.status.head=Serveri seisund -servers.manager.status.outside.pinging=Serveri seisund (kontrollitakse...) -servers.manager.status.outside.pong=Serveri seisund (%sms) -servers.manager.status.outside.error=Serveri seisund (ebaõnnestus) diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 711f14e388f..c5ccc3a3747 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1171,6 +1171,44 @@ selector.choose=選択 selector.choose_file=ファイルを選択します # selector.choose_directory= selector.custom=カスタム +server.delete=このサーバーを削除 +server.dnd.add=現在のインスタンスに追加 +server.dnd.add.instanceempty=追加に失敗しました +server.dnd.add.instanceempty.desc=追加できるゲームがありません。\nダウンロードページで新しいゲームをダウンロードするか、"インスタンス一覧" でゲームフォルダを切り替えることができます。 +# server.empty= +server.ip=サーバipアドレス +server.latency=現在の遅延 +server.manage=サーバー +server.manage.add=サーバーの追加 +server.manage.add.accept=追加 +server.manage.add.head=サーバーの追加 +server.manage.add.still=追加 +server.manage.copy.server.ip=IP アドレスをコピー +server.manage.copy.server.ip.ok.toast=サーバー IP アドレスをクリップボードにコピーしました +server.manage.copy.to.instance=このインスタンスにコピー +server.manage.delete=削除 +server.manage.show_all=全て表示 +server.manage.show_hide=ヒョウジヒヒョウジ +server.manage.edit=サーバーを編集 +server.manage.edit.accept=送信 +server.manage.edit.head=サーバーを編集 +server.manage.edit.still=強制送信 +server.manage.status=サーバの状態 +server.manage.status.error=現在のサーバステータスを取得できません +server.manage.status.head=サーバの状態 +server.manage.status.outside.error=サーバの状態 (失敗) +server.manage.status.outside.pinging=サーバの状態 (Ping中...) +server.manage.status.outside.pong=サーバの状態 (%sms) +server.motd=MOTD +server.name=サーバ名 +server.name.def=Minecraft Server +server.onlineplayers=オンライン +server.playversion=バージョン +server.tag.hide=隠しの +server.tag.hold.current=保有中 +server.type=サーバーの種類 +server.type.mod=%s (%,d mods) +server.type.vanilla=バニラ settings=設定 @@ -1606,42 +1644,3 @@ wizard.prev=< 前へ wizard.failed=失敗 wizard.finish=終了 wizard.next=次へ > - -servers.manage=サーバー -servers.show_all=全て表示 -servers.show_hide=ヒョウジヒヒョウジ -servers.manage.copy.server.ip=IP アドレスをコピー -servers.manage.copy.server.ip.ok.toast=サーバー IP アドレスをクリップボードにコピーしました -servers.manage.copy.to.instance=このインスタンスにコピー -servers.manage.delete=削除 -server.delete=このサーバーを削除 -server.tag.hide=隠しの -server.tag.hold.current=保有中 -# server.empty= -servers.manager.add=サーバーの追加 -servers.manager.add.head=サーバーの追加 -servers.manager.server.name=サーバ名 -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=サーバipアドレス -servers.manager.server.type=サーバーの種類 -servers.manager.server.version=バージョン -servers.manager.server.vanilla=バニラ -servers.manager.server.latency=現在の遅延 -servers.manager.server.players=オンライン -servers.manager.server.modtypeinfo=%s (%,d mods) -servers.manager.server.motd=MOTD -servers.manager.add.accept=追加 -servers.manager.add.still=追加 -servers.dnd.add=現在のインスタンスに追加 -servers.dnd.add.instanceempty=追加に失敗しました -servers.dnd.add.instanceempty.desc=追加できるゲームがありません。\nダウンロードページで新しいゲームをダウンロードするか、"インスタンス一覧" でゲームフォルダを切り替えることができます。 -servers.manager.edit=サーバーを編集 -servers.manager.edit.head=サーバーを編集 -servers.manager.edit.accept=送信 -servers.manager.edit.still=強制送信 -servers.manager.error.status=現在のサーバステータスを取得できません -servers.manager.status=サーバの状態 -servers.manager.status.head=サーバの状態 -servers.manager.status.outside.pinging=サーバの状態 (Ping中...) -servers.manager.status.outside.pong=サーバの状態 (%sms) -servers.manager.status.outside.error=サーバの状態 (失敗) diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index ef42a1c2f6f..69514cfde47 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1183,6 +1183,44 @@ selector.choose=擇 selector.choose_file=擇案 # selector.choose_directory= selector.custom=自定 +# server.delete= +# server.dnd.add= +# server.dnd.add.instanceempty= +# server.dnd.add.instanceempty.desc= +# server.empty= +# server.ip= +# server.latency= +# server.manage= +# server.manage.add= +# server.manage.add.accept= +# server.manage.add.head= +# server.manage.add.still= +# server.manage.copy.server.ip= +# server.manage.copy.server.ip.ok.toast= +# server.manage.copy.to.instance= +# server.manage.delete= +# server.manage.show_all= +# server.manage.show_hide= +# server.manage.edit= +# server.manage.edit.accept= +# server.manage.edit.head= +# server.manage.edit.still= +# server.manage.status= +# server.manage.status.error= +# server.manage.status.head= +# server.manage.status.outside.error= +# server.manage.status.outside.pinging= +# server.manage.status.outside.pong= +# server.motd= +# server.name= +# server.name.def= +# server.onlineplayers= +# server.playversion= +# server.tag.hide= +# server.tag.hold.current= +# server.type= +# server.type.mod= +# server.type.vanilla= settings=置設 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 697de846793..2497fe92657 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1307,6 +1307,44 @@ selector.choose=Выберите selector.choose_file=Выберите файл selector.choose_directory=Выберите папку selector.custom=Свой +server.delete=Удалить этот сервер +server.dnd.add=Добавить в текущий экземпляр +server.dnd.add.instanceempty=Не удалось добавить +server.dnd.add.instanceempty.desc=Нет игр, доступных для добавления.\nВы можете перейти на страницу загрузки, чтобы скачать новую игру, или переключить папку игры в списке "Экземпляры". +server.empty=Нет Серверы +server.ip=IP-адрес сервера +server.latency=Текущая задержка +server.manage=Серверы +server.manage.add=Добавить сервер +server.manage.add.accept=Добавить +server.manage.add.head=Добавить сервер +server.manage.add.still=Добавить +server.manage.copy.server.ip=Копировать IP-адрес +server.manage.copy.server.ip.ok.toast=IP-адрес сервера скопирован в буфер обмена +server.manage.copy.to.instance=Копировать в этот экземпляр +server.manage.delete=Удалить +server.manage.show_all=Показать все +server.manage.show_hide=Показать Скрытое +server.manage.edit=Редактировать сервер +server.manage.edit.accept=Подтвердить +server.manage.edit.head=Редактировать сервер +server.manage.edit.still=Все равно подтвердить +server.manage.status=Статус сервера +server.manage.status.error=Не удалось получить текущее состояние сервера +server.manage.status.head=Статус сервера +server.manage.status.outside.error=Статус сервера (не удалось) +server.manage.status.outside.pinging=Статус сервера (пинг...) +server.manage.status.outside.pong=Статус сервера (%sms) +server.motd=MOTD +server.name=Имя сервера +server.name.def=Minecraft Server +server.onlineplayers=Онлайн +server.playversion=Версия +server.tag.hide=Скрытое +server.tag.hold.current=ВладёныйТекущий +server.type=Тип сервера +server.type.mod=%s (%,d модов) +server.type.vanilla=Ваниль settings=Настройки @@ -1738,42 +1776,3 @@ wizard.prev=< Пред. wizard.failed=Не удалось wizard.finish=Готово wizard.next=След. > - -servers.manage=Серверы -servers.show_all=Показать все -servers.show_hide=Показать Скрытое -servers.manage.copy.server.ip=Копировать IP-адрес -servers.manage.copy.server.ip.ok.toast=IP-адрес сервера скопирован в буфер обмена -servers.manage.copy.to.instance=Копировать в этот экземпляр -servers.manage.delete=Удалить -server.delete=Удалить этот сервер -server.tag.hide=Скрытое -server.tag.hold.current=ВладёныйТекущий -server.empty=Нет Серверы -servers.manager.add=Добавить сервер -servers.manager.add.head=Добавить сервер -servers.manager.server.name=Имя сервера -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=IP-адрес сервера -servers.manager.server.type=Тип сервера -servers.manager.server.version=Версия -servers.manager.server.vanilla=Ваниль -servers.manager.server.latency=Текущая задержка -servers.manager.server.players=Онлайн -servers.manager.server.modtypeinfo=%s (%,d модов) -servers.manager.server.motd=MOTD -servers.manager.add.accept=Добавить -servers.manager.add.still=Добавить -servers.dnd.add=Добавить в текущий экземпляр -servers.dnd.add.instanceempty=Не удалось добавить -servers.dnd.add.instanceempty.desc=Нет игр, доступных для добавления.\nВы можете перейти на страницу загрузки, чтобы скачать новую игру, или переключить папку игры в списке "Экземпляры". -servers.manager.edit=Редактировать сервер -servers.manager.edit.head=Редактировать сервер -servers.manager.edit.accept=Подтвердить -servers.manager.edit.still=Все равно подтвердить -servers.manager.error.status=Не удалось получить текущее состояние сервера -servers.manager.status=Статус сервера -servers.manager.status.head=Статус сервера -servers.manager.status.outside.pinging=Статус сервера (пинг...) -servers.manager.status.outside.pong=Статус сервера (%sms) -servers.manager.status.outside.error=Статус сервера (не удалось) diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index ba6d09a5be6..091f40af6ec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1330,6 +1330,44 @@ selector.choose=Вибрати selector.choose_file=Вибрати файл selector.choose_directory=Виберіть каталог selector.custom=Власний +server.delete=Видалити цей сервер +server.dnd.add=Додати до поточного екземпляра +server.dnd.add.instanceempty=Не вдалося додати +server.dnd.add.instanceempty.desc=Немає доступних ігор для додавання.\nВи можете перейти на сторінку завантажень, щоб завантажити нову гру, або переключити папку гри в списку "Екземпляри". +server.empty=Немає Сервери +server.ip=IP-адреса сервера +server.latency=Поточна затримка +server.manage=Сервери +server.manage.add=Додати сервер +server.manage.add.accept=Додати +server.manage.add.head=Додати сервер +server.manage.add.still=Додати +server.manage.copy.server.ip=Копіювати IP-адресу +server.manage.copy.server.ip.ok.toast=IP-адреса сервера скопійована в буфер обміну +server.manage.copy.to.instance=Копіювати до поточного випадку +server.manage.delete=Вилучити +server.manage.show_all=Показати все +server.manage.show_hide=Показати Приховане +server.manage.edit=Редагувати сервер +server.manage.edit.accept=Підтвердити +server.manage.edit.head=Редагувати сервер +server.manage.edit.still=Все одно підтвердити +server.manage.status=Статус сервера +server.manage.status.error=Не вдалося отримати поточний стан сервера +server.manage.status.head=Статус сервера +server.manage.status.outside.error=Статус сервера (помилка) +server.manage.status.outside.pinging=Статус сервера (пінг...) +server.manage.status.outside.pong=Статус сервера (%sms) +server.motd=MOTD +server.name=Назва сервера +server.name.def=Minecraft Server +server.onlineplayers=Онлайн +server.playversion=Версія +server.tag.hide=Приховане +server.tag.hold.current=ВласнийПоточний +server.type=Тип сервера +server.type.mod=%s (%,d модів) +server.type.vanilla=Ваніль settings=Налаштування @@ -1769,42 +1807,3 @@ wizard.prev=< Попередній wizard.failed=Не вдалося wizard.finish=Завершити wizard.next=Наступний > - -servers.manage=Сервери -servers.show_all=Показати все -servers.show_hide=Показати Приховане -servers.manage.copy.server.ip=Копіювати IP-адресу -servers.manage.copy.server.ip.ok.toast=IP-адреса сервера скопійована в буфер обміну -servers.manage.copy.to.instance=Копіювати до поточного випадку -servers.manage.delete=Вилучити -server.delete=Видалити цей сервер -server.tag.hide=Приховане -server.tag.hold.current=ВласнийПоточний -server.empty=Немає Сервери -servers.manager.add=Додати сервер -servers.manager.add.head=Додати сервер -servers.manager.server.name=Назва сервера -servers.manager.server.name.def=Minecraft Server -servers.manager.server.ip=IP-адреса сервера -servers.manager.server.type=Тип сервера -servers.manager.server.version=Версія -servers.manager.server.vanilla=Ваніль -servers.manager.server.latency=Поточна затримка -servers.manager.server.players=Онлайн -servers.manager.server.modtypeinfo=%s (%,d модів) -servers.manager.server.motd=MOTD -servers.manager.add.accept=Додати -servers.manager.add.still=Додати -servers.dnd.add=Додати до поточного екземпляра -servers.dnd.add.instanceempty=Не вдалося додати -servers.dnd.add.instanceempty.desc=Немає доступних ігор для додавання.\nВи можете перейти на сторінку завантажень, щоб завантажити нову гру, або переключити папку гри в списку "Екземпляри". -servers.manager.edit=Редагувати сервер -servers.manager.edit.head=Редагувати сервер -servers.manager.edit.accept=Підтвердити -servers.manager.edit.still=Все одно підтвердити -servers.manager.error.status=Не вдалося отримати поточний стан сервера -servers.manager.status=Статус сервера -servers.manager.status.head=Статус сервера -servers.manager.status.outside.pinging=Статус сервера (пінг...) -servers.manager.status.outside.pong=Статус сервера (%sms) -servers.manager.status.outside.error=Статус сервера (помилка) 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 260f7de2c52..066c5ba3d91 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1187,6 +1187,44 @@ selector.choose=选择 selector.choose_file=选择文件 selector.choose_directory=选择文件夹 selector.custom=自定义 +server.delete=删除此服务器 +server.dnd.add=添加到当前实例 +server.dnd.add.instanceempty=添加失败 +server.dnd.add.instanceempty.desc=没有可添加的游戏。\n你可以前往“下载”页面下载新游戏,或在“实例列表”中切换游戏文件夹。 +server.empty=没有服务器 +server.ip=服务器地址 +server.latency=当前延迟 +server.manage=服务器管理 +server.manage.add=添加服务器 +server.manage.add.accept=添加 +server.manage.add.head=添加服务器 +server.manage.add.still=依然添加 +server.manage.copy.server.ip=复制地址 +server.manage.copy.server.ip.ok.toast=已将服务器地址复制到剪贴板 +server.manage.copy.to.instance=复制到此实例 +server.manage.delete=删除 +server.manage.show_all=显示全部 +server.manage.show_hide=显示隐藏 +server.manage.edit=编辑服务器 +server.manage.edit.accept=提交 +server.manage.edit.head=编辑服务器 +server.manage.edit.still=依然提交 +server.manage.status=服务器状态 +server.manage.status.error=无法获取目标服务器状态 +server.manage.status.head=服务器状态 +server.manage.status.outside.error=服务器状态 (失败) +server.manage.status.outside.pinging=服务器状态 (Pinging...) +server.manage.status.outside.pong=服务器状态 (%sms) +server.motd=MOTD +server.name=服务器名字 +server.name.def=Minecraft 服务器 +server.onlineplayers=在线人数 +server.playversion=版本 +server.tag.hide=隐藏 +server.tag.hold.current=当前实例 +server.type=服务器类型 +server.type.mod=%s (%,d mods) +server.type.vanilla=原版 settings=设置 @@ -1627,42 +1665,3 @@ wizard.prev=< 上一步 wizard.failed=失败 wizard.finish=完成 wizard.next=下一步 > - -servers.manage=服务器管理 -servers.show_all=显示全部 -servers.show_hide=显示隐藏 -servers.manage.copy.server.ip=复制地址 -servers.manage.copy.server.ip.ok.toast=已将服务器地址复制到剪贴板 -servers.manage.copy.to.instance=复制到此实例 -servers.manage.delete=删除 -server.delete=删除此服务器 -server.tag.hide=隐藏 -server.tag.hold.current=当前实例 -server.empty=没有服务器 -servers.manager.add=添加服务器 -servers.manager.add.head=添加服务器 -servers.manager.server.name=服务器名字 -servers.manager.server.name.def=Minecraft 服务器 -servers.manager.server.ip=服务器地址 -servers.manager.server.type=服务器类型 -servers.manager.server.version=版本 -servers.manager.server.vanilla=原版 -servers.manager.server.latency=当前延迟 -servers.manager.server.players=在线人数 -servers.manager.server.modtypeinfo=%s(%,d mods) -servers.manager.server.motd=MOTD -servers.manager.add.accept=添加 -servers.manager.add.still=依然添加 -servers.dnd.add=添加到当前实例 -servers.dnd.add.instanceempty=添加失败 -servers.dnd.add.instanceempty.desc=没有可添加的游戏。\n你可以前往“下载”页面下载新游戏,或在“实例列表”中切换游戏文件夹。 -servers.manager.edit=编辑服务器 -servers.manager.edit.head=编辑服务器 -servers.manager.edit.accept=提交 -servers.manager.edit.still=依然提交 -servers.manager.error.status=无法获取目标服务器状态 -servers.manager.status=服务器状态 -servers.manager.status.head=服务器状态 -servers.manager.status.outside.pinging=服务器状态 (Pinging...) -servers.manager.status.outside.pong=服务器状态 (%sms) -servers.manager.status.outside.error=服务器状态 (失败) 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 5604941f194..fa7ee0934d1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1181,6 +1181,44 @@ selector.choose=選取 selector.choose_file=選取檔案 selector.choose_directory=選取目錄 selector.custom=自訂 +server.delete=刪除此伺服器 +server.dnd.add=新增到目前實例 +server.dnd.add.instanceempty=新增失敗 +server.dnd.add.instanceempty.desc=沒有可新增的遊戲。\n你可以前往「下載」頁面下載新遊戲,或在「實例清單」中切換遊戲目錄。 +server.empty=沒有伺服器 +server.ip=伺服器位址 +server.latency=當前延遲 +server.manage=伺服器管理 +server.manage.add=新增伺服器 +server.manage.add.accept=新增 +server.manage.add.head=新增伺服器 +server.manage.add.still=依然新增 +server.manage.copy.server.ip=複製位址 +server.manage.copy.server.ip.ok.toast=已將伺服器位址複製到剪貼簿 +server.manage.copy.to.instance=複製到此實例 +server.manage.delete=刪除 +server.manage.show_all=顯示全部 +server.manage.show_hide=顯示隱藏 +server.manage.edit=編輯伺服器 +server.manage.edit.accept=提交 +server.manage.edit.head=編輯伺服器 +server.manage.edit.still=仍然提交 +server.manage.status=伺服器狀態 +server.manage.status.error=無法取得目標伺服器狀態 +server.manage.status.head=伺服器狀態 +server.manage.status.outside.error=伺服器狀態 (失敗) +server.manage.status.outside.pinging=伺服器狀態 (Pinging...) +server.manage.status.outside.pong=伺服器狀態 (%sms) +server.motd=MOTD +server.name=伺服器名稱 +server.name.def=Minecraft 伺服器 +server.onlineplayers=線上人數 +server.playversion=版本 +server.tag.hide=隱藏 +server.tag.hold.current=目前實例 +server.type=伺服器類型 +server.type.mod=%s (%,d 個模組) +server.type.vanilla=原版 settings=設定 @@ -1621,42 +1659,3 @@ wizard.prev=< 上一步 wizard.failed=失敗 wizard.finish=完成 wizard.next=下一步 > - -servers.manage=伺服器管理 -servers.show_all=顯示全部 -servers.show_hide=顯示隱藏 -servers.manage.copy.server.ip=複製位址 -servers.manage.copy.server.ip.ok.toast=已將伺服器位址複製到剪貼簿 -servers.manage.copy.to.instance=複製到此實例 -servers.manage.delete=刪除 -server.delete=刪除此伺服器 -server.tag.hide=隱藏 -server.tag.hold.current=目前實例 -server.empty=沒有伺服器 -servers.manager.add=新增伺服器 -servers.manager.add.head=新增伺服器 -servers.manager.server.name=伺服器名稱 -servers.manager.server.name.def=Minecraft 伺服器 -servers.manager.server.ip=伺服器位址 -servers.manager.server.type=伺服器類型 -servers.manager.server.version=版本 -servers.manager.server.vanilla=原版 -servers.manager.server.latency=當前延遲 -servers.manager.server.players=線上人數 -servers.manager.server.modtypeinfo=%s(%,d 個模組) -servers.manager.server.motd=MOTD -servers.manager.add.accept=新增 -servers.manager.add.still=依然新增 -servers.dnd.add=新增到目前實例 -servers.dnd.add.instanceempty=新增失敗 -servers.dnd.add.instanceempty.desc=沒有可新增的遊戲。\n你可以前往「下載」頁面下載新遊戲,或在「實例清單」中切換遊戲目錄。 -servers.manager.edit=編輯伺服器 -servers.manager.edit.head=編輯伺服器 -servers.manager.edit.accept=提交 -servers.manager.edit.still=仍然提交 -servers.manager.error.status=無法取得目標伺服器狀態 -servers.manager.status=伺服器狀態 -servers.manager.status.head=伺服器狀態 -servers.manager.status.outside.pinging=伺服器狀態 (Pinging...) -servers.manager.status.outside.pong=伺服器狀態 (%sms) -servers.manager.status.outside.error=伺服器狀態 (失敗) From 75a36f26445a9e5a4810f95bd30c412ee33950d5 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 29 Sep 2026 19:58:03 +0800 Subject: [PATCH 27/97] =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=8E=9F=E7=89=88?= =?UTF-8?q?=E6=82=AC=E6=B5=AE=E6=98=BE=E7=A4=BA=E7=8E=A9=E5=AE=B6=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BB=A5=E5=8F=8A=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E6=82=AC=E6=B5=AE=E6=98=BE=E7=A4=BAmod=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerStatusPane.java | 53 +++++++++++++++++-- .../resources/assets/lang/I18N.properties | 4 ++ .../resources/assets/lang/I18N_ar.properties | 4 ++ .../resources/assets/lang/I18N_de.properties | 4 ++ .../resources/assets/lang/I18N_es.properties | 4 ++ .../resources/assets/lang/I18N_ja.properties | 4 ++ .../resources/assets/lang/I18N_lzh.properties | 4 ++ .../resources/assets/lang/I18N_ru.properties | 4 ++ .../resources/assets/lang/I18N_uk.properties | 4 ++ .../assets/lang/I18N_zh_Hans.properties | 4 ++ .../assets/lang/I18N_zh_Hant.properties | 4 ++ .../jackhuang/hmcl/server/ServerStatus.java | 2 +- 12 files changed, 89 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index b2d286d2dd6..0787e08c53f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -40,6 +40,7 @@ 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.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; @@ -138,7 +139,7 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser refreshIfNoStatus(); } - private static VBox createStatCell(String titleText, String valueText, boolean showDivider) { + private static VBox createStatCell(String titleText, String valueText, boolean showDivider, String tooltip) { VBox cell = new VBox(3); cell.getStyleClass().add("server-status-stat-cell"); if (!showDivider) { @@ -156,6 +157,10 @@ private static VBox createStatCell(String titleText, String valueText, boolean s value.setWrapText(true); value.setMaxWidth(100); + if (tooltip != null) { + FXUtils.installFastTooltip(value, tooltip); + } + cell.getChildren().addAll(title, value); return cell; } @@ -245,11 +250,49 @@ public void replaceBody() { detailRow.setSpacing(0); detailRow.setMaxWidth(620); + String playersTooltip = null; + if (!status.players().samples().isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(String.join("\n", status.players().samples().stream() + .map(sample -> { + if (ServerStatus.Players.Sample.ANONYMOUS_PLAYER.equals(sample)) { + return i18n("server.anonymous_player"); + } + return MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(sample.name())); + }) + .toList()) + ); + + if (status.players().samples().size() < status.players().online()) { + sb.append("\n"); + sb.append(i18n("server.and_more_players", String.valueOf(status.players().online() - status.players().samples().size()))); + } + + playersTooltip = sb.toString(); + } + + String modsTooltip = null; + int showCount = 15; + if (status.modInfo() != null && !status.modInfo().modList().isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(String.join("\n", status.modInfo().modList().stream() + .limit(showCount) + .map(it -> it.modId() + " - " + it.version()) + .toList()) + ); + if (showCount < status.modInfo().modList().size()) { + sb.append("\n"); + sb.append(i18n("server.and_more_mods", String.valueOf(status.modInfo().modList().size() - showCount))); + } + + modsTooltip = sb.toString(); + } + var cells = List.of( - createStatCell(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), true), - createStatCell(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), true), - createStatCell(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true), - createStatCell(i18n("server.latency"), String.format("%,dms", status.networkLatency()), false) + createStatCell(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), true, modsTooltip), + createStatCell(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), true, playersTooltip), + createStatCell(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true, null), + createStatCell(i18n("server.latency"), String.format("%,dms", status.networkLatency()), false, null) ); detailRow.getChildren().addAll(cells); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 68f3fa53328..9cb41557ca5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1375,6 +1375,10 @@ selector.choose=Choose selector.choose_file=Choose file selector.choose_directory=Choose directory selector.custom=Custom + +server.and_more_mods=... and %s more ... +server.and_more_players=... and %s more ... +server.anonymous_player=Anonymous player server.delete=Delete this server server.dnd.add=Add to current instance server.dnd.add.instanceempty=Add failed diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 05d9992008a..f845d12e7f8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1375,6 +1375,10 @@ selector.choose=اختر selector.choose_file=اختر ملفاً # selector.choose_directory= selector.custom=مخصص + +server.and_more_mods=... و %s إضافية ... +server.and_more_players=... و %s لاعبًا إضافيًا ... +server.anonymous_player=لاعب مجهول server.delete=حذف هذا الخادم server.dnd.add=إضافة إلى هذه الحالة الحالية server.dnd.add.instanceempty=فشل الإضافة diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index b1fef703a9f..ec4cd879cb7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1374,6 +1374,10 @@ selector.choose=Auswählen selector.choose_file=Datei auswählen selector.choose_directory=Verzeichnis auswählen selector.custom=Benutzerdefiniert + +server.and_more_mods=... und %s weitere Mods ... +server.and_more_players=... und %s weitere Spieler ... +server.anonymous_player=Anonymer Spieler server.delete=Diesen Server löschen server.dnd.add=Zur aktuellen Instanz hinzufügen server.dnd.add.instanceempty=Hinzufügen fehlgeschlagen diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index c3c092590b0..93a660dcd29 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1367,6 +1367,10 @@ selector.choose=Elegir selector.choose_file=Elegir archivo # selector.choose_directory= selector.custom=Personalizar + +server.and_more_mods=... y %s mods más ... +server.and_more_players=... y %s jugadores más ... +server.anonymous_player=Jugador anónimo server.delete=Eliminar este servidor server.dnd.add=Añadir a la instancia actual server.dnd.add.instanceempty=Error al añadir diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index c5ccc3a3747..0eaa8ef9d4c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1171,6 +1171,10 @@ selector.choose=選択 selector.choose_file=ファイルを選択します # selector.choose_directory= selector.custom=カスタム + +server.and_more_mods=... さらに %s 個の mod ... +server.and_more_players=... さらに %s 人 ... +server.anonymous_player=匿名プレイヤー server.delete=このサーバーを削除 server.dnd.add=現在のインスタンスに追加 server.dnd.add.instanceempty=追加に失敗しました diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 69514cfde47..945d031c37d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1183,6 +1183,10 @@ selector.choose=擇 selector.choose_file=擇案 # selector.choose_directory= selector.custom=自定 + +# server.and_more_mods= +# server.and_more_players= +# server.anonymous_player= # server.delete= # server.dnd.add= # server.dnd.add.instanceempty= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 2497fe92657..685b82330ed 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1307,6 +1307,10 @@ selector.choose=Выберите selector.choose_file=Выберите файл selector.choose_directory=Выберите папку selector.custom=Свой + +server.and_more_mods=... и ещё %s модов ... +server.and_more_players=... и ещё %s игроков ... +server.anonymous_player=Анонимный игрок server.delete=Удалить этот сервер server.dnd.add=Добавить в текущий экземпляр server.dnd.add.instanceempty=Не удалось добавить diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 091f40af6ec..4764d3c6d37 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1330,6 +1330,10 @@ selector.choose=Вибрати selector.choose_file=Вибрати файл selector.choose_directory=Виберіть каталог selector.custom=Власний + +server.and_more_mods=... і ще %s модів ... +server.and_more_players=... і ще %s гравців ... +server.anonymous_player=Анонімний гравець server.delete=Видалити цей сервер server.dnd.add=Додати до поточного екземпляра server.dnd.add.instanceempty=Не вдалося додати 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 066c5ba3d91..03d0c2be9d3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1187,6 +1187,10 @@ selector.choose=选择 selector.choose_file=选择文件 selector.choose_directory=选择文件夹 selector.custom=自定义 + +server.and_more_mods=... 及其他 %s 个mods ... +server.and_more_players=... 及其他 %s 名玩家 ... +server.anonymous_player=匿名玩家 server.delete=删除此服务器 server.dnd.add=添加到当前实例 server.dnd.add.instanceempty=添加失败 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 fa7ee0934d1..f131156913a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1181,6 +1181,10 @@ selector.choose=選取 selector.choose_file=選取檔案 selector.choose_directory=選取目錄 selector.custom=自訂 + +server.and_more_mods=... 及其他 %s 個模組 ... +server.and_more_players=... 及其他 %s 名玩家 ... +server.anonymous_player=匿名玩家 server.delete=刪除此伺服器 server.dnd.add=新增到目前實例 server.dnd.add.instanceempty=新增失敗 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 0eb0ca40718..5fa138928e7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -50,7 +50,7 @@ public record Sample( @NotNull String name, @NotNull UUID id ) { - + public static Sample ANONYMOUS_PLAYER = new Sample("Anonymous Player", new UUID(0, 0)); } } From cc1b9456009e0403a06413125c3c26f88967b934 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 29 Sep 2026 22:21:07 +0800 Subject: [PATCH 28/97] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/src/main/resources/assets/lang/I18N.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_ar.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_de.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_es.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_ja.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_ru.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_uk.properties | 4 ++-- HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties | 6 +++--- HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties | 4 ++-- 9 files changed, 19 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9cb41557ca5..b9ebbdc0ede 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1376,8 +1376,8 @@ selector.choose_file=Choose file selector.choose_directory=Choose directory selector.custom=Custom -server.and_more_mods=... and %s more ... -server.and_more_players=... and %s more ... +server.and_more_mods=... and %s more mods +server.and_more_players=... and %s more players server.anonymous_player=Anonymous player server.delete=Delete this server server.dnd.add=Add to current instance diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index f845d12e7f8..bb471a89c85 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1376,8 +1376,8 @@ selector.choose_file=اختر ملفاً # selector.choose_directory= selector.custom=مخصص -server.and_more_mods=... و %s إضافية ... -server.and_more_players=... و %s لاعبًا إضافيًا ... +server.and_more_mods=... و %s إضافية +server.and_more_players=... و %s لاعبًا إضافيًا server.anonymous_player=لاعب مجهول server.delete=حذف هذا الخادم server.dnd.add=إضافة إلى هذه الحالة الحالية diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index ec4cd879cb7..f56452af784 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1375,8 +1375,8 @@ selector.choose_file=Datei auswählen selector.choose_directory=Verzeichnis auswählen selector.custom=Benutzerdefiniert -server.and_more_mods=... und %s weitere Mods ... -server.and_more_players=... und %s weitere Spieler ... +server.and_more_mods=... und %s weitere Mods +server.and_more_players=... und %s weitere Spieler server.anonymous_player=Anonymer Spieler server.delete=Diesen Server löschen server.dnd.add=Zur aktuellen Instanz hinzufügen diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 93a660dcd29..714ceaf521b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1368,8 +1368,8 @@ selector.choose_file=Elegir archivo # selector.choose_directory= selector.custom=Personalizar -server.and_more_mods=... y %s mods más ... -server.and_more_players=... y %s jugadores más ... +server.and_more_mods=... y %s mods más +server.and_more_players=... y %s jugadores más server.anonymous_player=Jugador anónimo server.delete=Eliminar este servidor server.dnd.add=Añadir a la instancia actual diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 0eaa8ef9d4c..8cbb44abda6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1172,8 +1172,8 @@ selector.choose_file=ファイルを選択します # selector.choose_directory= selector.custom=カスタム -server.and_more_mods=... さらに %s 個の mod ... -server.and_more_players=... さらに %s 人 ... +server.and_more_mods=... さらに %s 個の mod +server.and_more_players=... さらに %s 人 server.anonymous_player=匿名プレイヤー server.delete=このサーバーを削除 server.dnd.add=現在のインスタンスに追加 diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 685b82330ed..12a40d5a985 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1308,8 +1308,8 @@ selector.choose_file=Выберите файл selector.choose_directory=Выберите папку selector.custom=Свой -server.and_more_mods=... и ещё %s модов ... -server.and_more_players=... и ещё %s игроков ... +server.and_more_mods=... и ещё %s модов +server.and_more_players=... и ещё %s игроков server.anonymous_player=Анонимный игрок server.delete=Удалить этот сервер server.dnd.add=Добавить в текущий экземпляр diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 4764d3c6d37..39bd330704d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1331,8 +1331,8 @@ selector.choose_file=Вибрати файл selector.choose_directory=Виберіть каталог selector.custom=Власний -server.and_more_mods=... і ще %s модів ... -server.and_more_players=... і ще %s гравців ... +server.and_more_mods=... і ще %s модів +server.and_more_players=... і ще %s гравців server.anonymous_player=Анонімний гравець server.delete=Видалити цей сервер server.dnd.add=Додати до поточного екземпляра 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 03d0c2be9d3..2b4a83637ec 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1188,8 +1188,8 @@ selector.choose_file=选择文件 selector.choose_directory=选择文件夹 selector.custom=自定义 -server.and_more_mods=... 及其他 %s 个mods ... -server.and_more_players=... 及其他 %s 名玩家 ... +server.and_more_mods=... 及其他 %s 个模组 +server.and_more_players=... 及其他 %s 名玩家 server.anonymous_player=匿名玩家 server.delete=删除此服务器 server.dnd.add=添加到当前实例 @@ -1227,7 +1227,7 @@ server.playversion=版本 server.tag.hide=隐藏 server.tag.hold.current=当前实例 server.type=服务器类型 -server.type.mod=%s (%,d mods) +server.type.mod=%s (%,d 个模组) server.type.vanilla=原版 settings=设置 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 f131156913a..96073d5fddd 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1182,8 +1182,8 @@ selector.choose_file=選取檔案 selector.choose_directory=選取目錄 selector.custom=自訂 -server.and_more_mods=... 及其他 %s 個模組 ... -server.and_more_players=... 及其他 %s 名玩家 ... +server.and_more_mods=... 及其他 %s 個模組 +server.and_more_players=... 及其他 %s 名玩家 server.anonymous_player=匿名玩家 server.delete=刪除此伺服器 server.dnd.add=新增到目前實例 From a4e18ee06383ae5743a337eafe71efd2548e57f3 Mon Sep 17 00:00:00 2001 From: APC Date: Tue, 29 Sep 2026 22:27:15 +0800 Subject: [PATCH 29/97] Update HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties Co-authored-by: 3gf8jv4dv <3gf8jv4dv@gmail.com> --- HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2b4a83637ec..0a75bd0f0e6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1217,7 +1217,7 @@ server.manage.status=服务器状态 server.manage.status.error=无法获取目标服务器状态 server.manage.status.head=服务器状态 server.manage.status.outside.error=服务器状态 (失败) -server.manage.status.outside.pinging=服务器状态 (Pinging...) +server.manage.status.outside.pinging=服务器状态 (检测中...) server.manage.status.outside.pong=服务器状态 (%sms) server.motd=MOTD server.name=服务器名字 From 4b69adf0615d0b81cc3bed694278d76580beb2cb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 30 Sep 2026 03:16:42 +0800 Subject: [PATCH 30/97] =?UTF-8?q?=E9=99=90=E5=88=B6ping=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=95=B0=E9=87=8F,=20=E5=88=97=E8=A1=A8=E5=92=8C=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=BC=B9=E7=AA=97=E5=85=B1=E7=94=A8Property?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 9 +- .../server/ObservableServerStatus.java | 98 +++++++++++++++++++ .../ui/instances/server/ServerListPage.java | 57 ++++++----- .../ui/instances/server/ServerStatusPane.java | 72 ++++++-------- .../org/jackhuang/hmcl/server/Server.java | 1 - ...tusGetter.java => ServerStatusPinger.java} | 6 +- 6 files changed, 161 insertions(+), 82 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{ServerStatusGetter.java => ServerStatusPinger.java} (98%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index cf7eb109fa6..4254cb053f6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -28,7 +28,7 @@ import javafx.scene.control.TextFormatter; import javafx.scene.layout.*; import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusGetter; +import org.jackhuang.hmcl.server.ServerStatusPinger; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -42,7 +42,6 @@ import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; import static org.jackhuang.hmcl.ui.FXUtils.setValidateWhileTextChanged; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class EditServerPane extends TransitionPane implements DialogAware { private final Consumer handleCallback; @@ -183,10 +182,8 @@ private void onAdd() { body.setDisable(true); Task.supplyAsync(Schedulers.io(), () -> - ServerStatusGetter.getStatus(serverIP) - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) - LOG.warning("Failed to fetch server status.", exception); + ServerStatusPinger.getStatus(serverIP) + ).whenComplete(Schedulers.javafx(), (result, ignored) -> { ServerStatus status = result.getIfSuccess(); if (status == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java new file mode 100644 index 00000000000..96128f1d1dc --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java @@ -0,0 +1,98 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances.server; + +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.beans.property.ReadOnlyObjectWrapper; +import org.jackhuang.hmcl.server.ServerStatusPinger; +import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.task.Schedulers; +import org.jackhuang.hmcl.task.Task; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +public class ObservableServerStatus { + + private static final Semaphore PING_SEMAPHORE = new Semaphore(5); + + private final String serverIp; + private final ReadOnlyObjectWrapper resultProperty = new ReadOnlyObjectWrapper<>(); + private final ReadOnlyBooleanWrapper pingingProperty = new ReadOnlyBooleanWrapper(); + private final AtomicInteger refreshId = new AtomicInteger(); + + public ObservableServerStatus(String serverIp) { + this.serverIp = serverIp; + } + + public void refreshIfNoResultAsync(boolean urgent) { + if (resultProperty.get() == null) { + refreshAsync(urgent); + } + } + + public void refreshAsync(boolean urgent) { + Task.runAsync(Schedulers.javafx(), () -> { + + if (pingingProperty.get() && !urgent) { + return; + } + + int currentRefreshId = refreshId.incrementAndGet(); + pingingProperty.set(true); + + Task.supplyAsync(Schedulers.io(), () -> { + // expired. + if (currentRefreshId != refreshId.get()) return null; + + if (urgent) { + return ServerStatusPinger.getStatus(serverIp); + } + + PING_SEMAPHORE.acquireUninterruptibly(); + try { + // expired. + if (currentRefreshId != refreshId.get()) return null; + + return ServerStatusPinger.getStatus(serverIp); + } finally { + PING_SEMAPHORE.release(); + } + }).whenComplete(Schedulers.javafx(), (result, ignored) -> { + // expired. + if (currentRefreshId != refreshId.get()) return; + + try { + resultProperty.set(result); + } finally { + pingingProperty.set(false); + } + }).start(); + }).start(); + } + + public ReadOnlyBooleanProperty pingingProperty() { + return pingingProperty.getReadOnlyProperty(); + } + + public ReadOnlyObjectProperty resultProperty() { + return resultProperty.getReadOnlyProperty(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 4c1303498bb..e85fcd02e28 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -42,7 +41,6 @@ import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusGetter; import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -56,7 +54,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.atomic.AtomicBoolean; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -100,7 +97,7 @@ public void launchAndEnterServer(ServerHolder holder) { public void showServerStatus(ServerHolder holder) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.serverStatusResultWrapper, holder.server))); + runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.observableServerStatus, holder.server))); } } @@ -265,7 +262,8 @@ private static final class ServerListCell extends ListCell applyServerStatusResult(holder, result)); + holder.observableServerStatus.refreshIfNoResultAsync(false); + + applyServerStatusResult(holder, holder.observableServerStatus.resultProperty().get()); + serverStatusResultValueSubscription = holder.observableServerStatus.resultProperty().subscribe(result -> applyServerStatusResult(holder, result)); + + applyServerPinging(holder, holder.observableServerStatus.pingingProperty().get()); + serverStatusPingingValueSubscription = holder.observableServerStatus.pingingProperty().subscribe(pinging -> applyServerPinging(holder, pinging)); } } - private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { - if (result == null) { + private void applyServerPinging(ServerHolder holder, boolean pinging) { + if (pinging) { serverNetworkLatencyPane.ping(); statusBtnTooltip.setText(i18n("server.manage.status.outside.pinging")); + } + } + + private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { + if (result == null) { + // pinging..., skip return; } ServerStatus serverStatus = result.getIfSuccess(); @@ -526,28 +539,14 @@ public static final class ServerHolder { final int inDatPathSlot; final @NotNull IconedServer server; - final @NotNull ReadOnlyObjectWrapper serverStatusResultWrapper = new ReadOnlyObjectWrapper<>(null); - private final AtomicBoolean serverStatusComputed = new AtomicBoolean(false); - - public void startGetServerStatusAsync() { - if (serverStatusResultWrapper.get() != null) return; - if (serverStatusComputed.compareAndSet(false, true)) { - reGetServerStatusAsync(); - } - } - - public void reGetServerStatusAsync() { - Task.supplyAsync(Schedulers.io(), () -> ServerStatusGetter.getStatus(server.getIp())) - .whenComplete(Schedulers.javafx(), (result, ignored) -> { - serverStatusResultWrapper.set(result); - }).start(); - } + final @NotNull ObservableServerStatus observableServerStatus; public ServerHolder(@NotNull Path fromServersDatFilePath, @NotNull List cachedServers, int inDatPathSlot, @NotNull IconedServer server) { this.fromServersDatFilePath = fromServersDatFilePath; this.inDatPathSlot = inDatPathSlot; this.cachedServers = cachedServers; this.server = server; + this.observableServerStatus = new ObservableServerStatus(server.getIp()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 0787e08c53f..c25b8d77dc5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -20,9 +20,6 @@ import com.google.gson.JsonPrimitive; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; -import javafx.beans.property.ReadOnlyObjectWrapper; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.WeakChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.Label; @@ -34,13 +31,11 @@ import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusGetter; import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.setting.GameDirectoryManager; -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.WeakListenerHolder; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; @@ -57,18 +52,16 @@ public class ServerStatusPane extends TransitionPane implements DialogAware { private final SpinnerPane refreshSpinner = new SpinnerPane(); private final JFXDialogLayout rootLayout = new JFXDialogLayout(); private final Label lblErrorMessage = new Label(); - private final ReadOnlyObjectWrapper serverStatusResultWrapper; - private final ChangeListener listener = (ignored0, ignored1, newResult) -> { - replaceBody(); - }; + private final ObservableServerStatus observableServerStatus; + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - public ServerStatusPane(ReadOnlyObjectWrapper reference, ServerListPage.IconedServer iconedServer) { + public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedServer iconedServer) { this(reference, iconedServer, false); } - public ServerStatusPane(ReadOnlyObjectWrapper reference, ServerListPage.IconedServer iconedServer, boolean fromDnD) { + public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedServer iconedServer, boolean fromDnD) { this.iconedServer = iconedServer; - this.serverStatusResultWrapper = Objects.requireNonNullElseGet(reference, () -> new ReadOnlyObjectWrapper<>(null)); + this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(iconedServer.getIp())); getStyleClass().add("skin-pane"); getChildren().setAll(rootLayout); @@ -77,7 +70,7 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser { JFXButton refreshBtn = new JFXButton(i18n("button.refresh")); refreshBtn.getStyleClass().add("dialog-refresh"); - refreshBtn.setOnAction(e -> refresh()); + refreshBtn.setOnAction(e -> observableServerStatus.refreshAsync(true)); refreshSpinner.getStyleClass().add("small-spinner-pane"); refreshSpinner.setContent(refreshBtn); } @@ -134,9 +127,13 @@ public ServerStatusPane(ReadOnlyObjectWrapper reference, Ser lblErrorMessage.setMaxWidth(400); rootLayout.getActions().addAll(lblErrorMessage, actions); - replaceBody(); - serverStatusResultWrapper.addListener(new WeakChangeListener<>(listener)); - refreshIfNoStatus(); + applyResult(observableServerStatus.resultProperty().get()); + listenerHolder.add(FXUtils.onWeakChangeAndOperate(observableServerStatus.resultProperty(), this::applyResult)); + + applyPinging(observableServerStatus.pingingProperty().get()); + listenerHolder.add(FXUtils.onWeakChangeAndOperate(observableServerStatus.pingingProperty(), this::applyPinging)); + + observableServerStatus.refreshIfNoResultAsync(true); } private static VBox createStatCell(String titleText, String valueText, boolean showDivider, String tooltip) { @@ -165,14 +162,27 @@ private static VBox createStatCell(String titleText, String valueText, boolean s return cell; } - public void replaceBody() { - ServerStatusResult statusResult = serverStatusResultWrapper.get(); + public void applyPinging(boolean pinging) { + if (pinging) { + lblErrorMessage.setText(""); + refreshSpinner.showSpinner(); + } else { + refreshSpinner.hideSpinner(); + } + } + + public void applyResult(ServerStatusResult statusResult) { ServerStatus status; if (statusResult == null) { + // pinging..., continue status = null; } else { + // ping failed status = statusResult.getIfSuccess(); + if (status == null) { + lblErrorMessage.setText(i18n("server.manage.status.error")); + } } Image serverIcon = iconedServer.iconImage; @@ -180,15 +190,6 @@ public void replaceBody() { serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); } - refreshSpinner.hideSpinner(); - if (statusResult != null) { - if (statusResult.exceptionIfFailure() != null) { - lblErrorMessage.setText(i18n("server.manage.status.error")); - } else { - lblErrorMessage.setText(""); - } - } - VBox root = new VBox(); root.setSpacing(10); HBox serverInfoBox = new HBox(); @@ -302,21 +303,6 @@ public void replaceBody() { rootLayout.setBody(root); } - private void refreshIfNoStatus() { - if (serverStatusResultWrapper.get() == null) refresh(); - } - - private void refresh() { - lblErrorMessage.setText(""); - refreshSpinner.showSpinner(); - - Task.supplyAsync(Schedulers.io(), () -> - ServerStatusGetter.getStatus(iconedServer.getIp()) - ).whenComplete(Schedulers.javafx(), (result, ignored) -> { - serverStatusResultWrapper.set(result); - }).start(); - } - private void onClose() { fireEvent(new DialogCloseEvent()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 0d2d84774ee..77e2faf5f0b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -31,7 +31,6 @@ import static org.jackhuang.hmcl.util.NBTUtils.writeCompressed; public class Server { - private final boolean acceptTextures; private final boolean hidden; private final @Nullable String icon; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java similarity index 98% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index 73577bd0b72..a94c461e79a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusGetter.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -32,13 +32,13 @@ // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Minecraft_Forge_Handshake -public final class ServerStatusGetter { - private ServerStatusGetter() { +public final class ServerStatusPinger { + private ServerStatusPinger() { } public static @NotNull ServerStatusResult getStatus(String serverIp) throws IOException { - return ServerStatusGetter.getStatus(ServerAddress.parseAddress(serverIp)); + return ServerStatusPinger.getStatus(ServerAddress.parseAddress(serverIp)); } private static @NotNull ServerStatusResult getStatus(ServerAddress address) throws IOException { From 9fe33eb017872aa5c089a5c163e085fe655e4050 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 30 Sep 2026 03:26:20 +0800 Subject: [PATCH 31/97] =?UTF-8?q?=E5=BD=93=E8=8E=B7=E5=8F=96=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81=E5=A4=B1=E8=B4=A5=E5=90=8E?= =?UTF-8?q?=EF=BC=8C=E6=89=93=E5=BC=80=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E7=95=8C=E9=9D=A2=E6=97=B6=E9=87=8D=E6=96=B0=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ObservableServerStatus.java | 7 +++++++ .../hmcl/ui/instances/server/ServerStatusPane.java | 2 +- .../org/jackhuang/hmcl/server/ServerStatusResult.java | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java index 96128f1d1dc..d0e087c6a7d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java @@ -48,6 +48,13 @@ public void refreshIfNoResultAsync(boolean urgent) { } } + public void refreshIfNoResultOrFailedAsync(boolean urgent) { + ServerStatusResult result = resultProperty.get(); + if (result == null || result.isFailure()) { + refreshAsync(urgent); + } + } + public void refreshAsync(boolean urgent) { Task.runAsync(Schedulers.javafx(), () -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index c25b8d77dc5..94468c96cbc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -133,7 +133,7 @@ public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedS applyPinging(observableServerStatus.pingingProperty().get()); listenerHolder.add(FXUtils.onWeakChangeAndOperate(observableServerStatus.pingingProperty(), this::applyPinging)); - observableServerStatus.refreshIfNoResultAsync(true); + observableServerStatus.refreshIfNoResultOrFailedAsync(true); } private static VBox createStatCell(String titleText, String valueText, boolean showDivider, String tooltip) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 31f0a81b6f0..4a810c975a3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -42,6 +42,14 @@ public static ServerStatusResult failure(Exception exception) { return null; } + public boolean isSuccess() { + return this instanceof SuccessResult; + } + + public boolean isFailure() { + return this instanceof FailureResult; + } + private static final class SuccessResult extends ServerStatusResult { public final ServerStatus serverStatus; From 33bb48748a6043abe78d22c83872466ddea1cf73 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 09:03:32 +0800 Subject: [PATCH 32/97] =?UTF-8?q?=E9=80=82=E9=85=8D26.4=E5=BF=AB=E7=85=A71?= =?UTF-8?q?=E6=96=B0=E8=BF=9E=E6=8E=A5=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 11 +- .../ui/instances/server/ServerListPage.java | 9 +- .../ui/instances/server/ServerStatusPane.java | 9 +- .../resources/assets/lang/I18N.properties | 4 + .../resources/assets/lang/I18N_ar.properties | 4 + .../resources/assets/lang/I18N_de.properties | 4 + .../resources/assets/lang/I18N_es.properties | 4 + .../resources/assets/lang/I18N_ja.properties | 4 + .../resources/assets/lang/I18N_lzh.properties | 4 + .../resources/assets/lang/I18N_ru.properties | 4 + .../resources/assets/lang/I18N_uk.properties | 4 + .../assets/lang/I18N_zh_Hans.properties | 4 + .../assets/lang/I18N_zh_Hant.properties | 4 + .../jackhuang/hmcl/server/ServerAddress.java | 79 ---------- .../hmcl/server/ServerStatusPinger.java | 62 ++++---- .../hmcl/server/ServerStatusResult.java | 28 ++-- .../hmcl/server/resolver/HostAndIp.java | 38 +++++ .../resolver/MojangBlockServerChecker.java | 144 ++++++++++++++++++ .../hmcl/server/resolver/ServerAddress.java | 110 +++++++++++++ .../resolver/ServerAddressResolveResult.java | 126 +++++++++++++++ .../ServerDnsSrvRedirector.java | 38 +++-- 21 files changed, 555 insertions(+), 139 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{ => resolver}/ServerDnsSrvRedirector.java (62%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 4254cb053f6..660af5da23b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -29,6 +29,7 @@ import javafx.scene.layout.*; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusPinger; +import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.TransitionPane; @@ -37,6 +38,7 @@ import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import java.util.Objects; import java.util.function.Consumer; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -185,7 +187,7 @@ private void onAdd() { ServerStatusPinger.getStatus(serverIP) ).whenComplete(Schedulers.javafx(), (result, ignored) -> { - ServerStatus status = result.getIfSuccess(); + ServerStatus status = result.getIfSucceed(); if (status == null) { spinner.hideSpinner(); btnAccept.setText(switch (type) { @@ -203,7 +205,12 @@ private void onAdd() { btnCancel.setOnAction(e -> { initPaneContents(); }); - lblErrorMessage.setText(i18n("server.manage.status.error")); + ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); + lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { + case EXCEPTION -> i18n("server.manage.status.error"); + case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); + case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); + }); } else { fireEvent(new DialogCloseEvent()); if (reference != null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index e85fcd02e28..fc5f67b8894 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -425,10 +425,15 @@ private void applyServerStatusResult(ServerHolder holder, ServerStatusResult res // pinging..., skip return; } - ServerStatus serverStatus = result.getIfSuccess(); + ServerStatus serverStatus = result.getIfSucceed(); if (serverStatus == null) { serverNetworkLatencyPane.error(); - statusBtnTooltip.setText(i18n("server.manage.status.outside.error")); + ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); + statusBtnTooltip.setText(switch (Objects.requireNonNull(reason)) { + case EXCEPTION -> i18n("server.manage.status.outside.error"); + case UNKNOWN_HOST -> i18n("server.manage.status.outside.error.unknownhost"); + case BLOCKED_BY_MOJANG -> i18n("server.manage.status.outside.error.blocked"); + }); } else { serverNetworkLatencyPane.pong(serverStatus.networkLatency()); statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 94468c96cbc..866a6c4576b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -179,9 +179,14 @@ public void applyResult(ServerStatusResult statusResult) { status = null; } else { // ping failed - status = statusResult.getIfSuccess(); + status = statusResult.getIfSucceed(); if (status == null) { - lblErrorMessage.setText(i18n("server.manage.status.error")); + ServerStatusResult.FailureResult.Reason reason = statusResult.getFailureReasonIfFailed(); + lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { + case EXCEPTION -> i18n("server.manage.status.error"); + case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); + case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); + }); } } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b9ebbdc0ede..5d08868f2d1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1403,8 +1403,12 @@ server.manage.edit.head=Edit Server server.manage.edit.still=Still Submit server.manage.status=Server Status server.manage.status.error=Unable to fetch the current server status +server.manage.status.error.blocked=The current server has been blocked +server.manage.status.error.unknownhost=Unknown host server.manage.status.head=Server Status server.manage.status.outside.error=Server Status (Failed) +server.manage.status.outside.error.blocked=Server Status (Failed, Blocked) +server.manage.status.outside.error.unknownhost=Server Status (Failed, Unknown host) server.manage.status.outside.pinging=Server Status (Pinging...) server.manage.status.outside.pong=Server Status (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index bb471a89c85..773de2825c8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1403,8 +1403,12 @@ server.manage.edit.head=تعديل الخادم server.manage.edit.still=الإرسال على أي حال server.manage.status=حالة الخادم server.manage.status.error=غير قادر على الحصول على حالة الخادم الحالية +server.manage.status.error.blocked=تم حظر الخادم الحالي +server.manage.status.error.unknownhost=المضيف غير معروف server.manage.status.head=حالة الخادم server.manage.status.outside.error=حالة الخادم (فشل) +server.manage.status.outside.error.blocked=حالة الخادم (فشل، محظور) +server.manage.status.outside.error.unknownhost=حالة الخادم (فشل، المضيف غير معروف) server.manage.status.outside.pinging=حالة الخادم (جارٍ الفحص...) server.manage.status.outside.pong=حالة الخادم (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index f56452af784..e2c83be7c2a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1402,8 +1402,12 @@ server.manage.edit.head=Server bearbeiten server.manage.edit.still=Trotzdem Übermitteln server.manage.status=Serverstatus server.manage.status.error=Der aktuelle Serverstatus kann nicht abgerufen werden +server.manage.status.error.blocked=Der aktuelle Server wurde blockiert +server.manage.status.error.unknownhost=Unbekannter Host server.manage.status.head=Serverstatus server.manage.status.outside.error=Serverstatus (Fehlgeschlagen) +server.manage.status.outside.error.blocked=Serverstatus (Fehlgeschlagen, blockiert) +server.manage.status.outside.error.unknownhost=Serverstatus (Fehlgeschlagen, unbekannter Host) server.manage.status.outside.pinging=Serverstatus (Pingt...) server.manage.status.outside.pong=Serverstatus (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 714ceaf521b..db14d8d81c5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1395,8 +1395,12 @@ server.manage.edit.head=Editar servidor server.manage.edit.still=Enviar igualmente server.manage.status=Serveri seisund server.manage.status.error=Serveri praeguse seisundi leidmine nurjus +server.manage.status.error.blocked=Praegune server on blokeeritud +server.manage.status.error.unknownhost=Tundmatu host server.manage.status.head=Serveri seisund server.manage.status.outside.error=Serveri seisund (ebaõnnestus) +server.manage.status.outside.error.blocked=Serveri seisund (ebaõnnestus, blokeeritud) +server.manage.status.outside.error.unknownhost=Serveri seisund (ebaõnnestus, tundmatu host) server.manage.status.outside.pinging=Serveri seisund (kontrollitakse...) server.manage.status.outside.pong=Serveri seisund (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 8cbb44abda6..7f1d68250ef 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1199,8 +1199,12 @@ server.manage.edit.head=サーバーを編集 server.manage.edit.still=強制送信 server.manage.status=サーバの状態 server.manage.status.error=現在のサーバステータスを取得できません +server.manage.status.error.blocked=現在のサーバーはブロックされています +server.manage.status.error.unknownhost=不明なホスト server.manage.status.head=サーバの状態 server.manage.status.outside.error=サーバの状態 (失敗) +server.manage.status.outside.error.blocked=サーバの状態 (失敗、ブロック) +server.manage.status.outside.error.unknownhost=サーバの状態 (失敗、不明なホスト) server.manage.status.outside.pinging=サーバの状態 (Ping中...) server.manage.status.outside.pong=サーバの状態 (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 945d031c37d..3f33ccfc549 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1211,8 +1211,12 @@ selector.custom=自定 # server.manage.edit.still= # server.manage.status= # server.manage.status.error= +# server.manage.status.error.blocked= +# server.manage.status.error.unknownhost= # server.manage.status.head= # server.manage.status.outside.error= +# server.manage.status.outside.error.blocked= +# server.manage.status.outside.error.unknownhost= # server.manage.status.outside.pinging= # server.manage.status.outside.pong= # server.motd= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 12a40d5a985..758b0444851 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1335,8 +1335,12 @@ server.manage.edit.head=Редактировать сервер server.manage.edit.still=Все равно подтвердить server.manage.status=Статус сервера server.manage.status.error=Не удалось получить текущее состояние сервера +server.manage.status.error.blocked=Текущий сервер был заблокирован +server.manage.status.error.unknownhost=Неизвестный хост server.manage.status.head=Статус сервера server.manage.status.outside.error=Статус сервера (не удалось) +server.manage.status.outside.error.blocked=Статус сервера (не удалось, заблокирован) +server.manage.status.outside.error.unknownhost=Статус сервера (не удалось, неизвестный хост) server.manage.status.outside.pinging=Статус сервера (пинг...) server.manage.status.outside.pong=Статус сервера (%sms) server.motd=MOTD diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 39bd330704d..edfe42a07fa 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1358,8 +1358,12 @@ server.manage.edit.head=Редагувати сервер server.manage.edit.still=Все одно підтвердити server.manage.status=Статус сервера server.manage.status.error=Не вдалося отримати поточний стан сервера +server.manage.status.error.blocked=Поточний сервер заблоковано +server.manage.status.error.unknownhost=Невідомий хост server.manage.status.head=Статус сервера server.manage.status.outside.error=Статус сервера (помилка) +server.manage.status.outside.error.blocked=Статус сервера (помилка, заблоковано) +server.manage.status.outside.error.unknownhost=Статус сервера (помилка, невідомий хост) server.manage.status.outside.pinging=Статус сервера (пінг...) server.manage.status.outside.pong=Статус сервера (%sms) server.motd=MOTD 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 0a75bd0f0e6..efd058d7014 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1215,8 +1215,12 @@ server.manage.edit.head=编辑服务器 server.manage.edit.still=依然提交 server.manage.status=服务器状态 server.manage.status.error=无法获取目标服务器状态 +server.manage.status.error.blocked=目标服务器已被禁止 +server.manage.status.error.unknownhost=未知的主机 server.manage.status.head=服务器状态 server.manage.status.outside.error=服务器状态 (失败) +server.manage.status.outside.error.blocked=服务器状态 (失败,被禁止) +server.manage.status.outside.error.unknownhost=服务器状态 (失败,未知的主机) server.manage.status.outside.pinging=服务器状态 (检测中...) server.manage.status.outside.pong=服务器状态 (%sms) server.motd=MOTD 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 96073d5fddd..1f362e9c080 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1209,8 +1209,12 @@ server.manage.edit.head=編輯伺服器 server.manage.edit.still=仍然提交 server.manage.status=伺服器狀態 server.manage.status.error=無法取得目標伺服器狀態 +server.manage.status.error.blocked=目標伺服器已被封鎖 +server.manage.status.error.unknownhost=未知主機 server.manage.status.head=伺服器狀態 server.manage.status.outside.error=伺服器狀態 (失敗) +server.manage.status.outside.error.blocked=伺服器狀態 (失敗,已封鎖) +server.manage.status.outside.error.unknownhost=伺服器狀態 (失敗,未知主機) server.manage.status.outside.pinging=伺服器狀態 (Pinging...) server.manage.status.outside.pong=伺服器狀態 (%sms) server.motd=MOTD diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java deleted file mode 100644 index 5b9201a35f7..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerAddress.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.server; - -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.util.Objects; - -public record ServerAddress( - String host, - int port -) { - public static int parsePort(final String str) { - try { - return Integer.parseInt(str.trim()); - } catch (Exception var2) { - return 25565; - } - } - - public static ServerAddress parseAddress(String ipStr) throws IOException { - if (ipStr == null || ipStr.isEmpty()) throw new IOException("server ip is empty."); - - String host = ipStr; - int port = 25565; - - int lastIndexOf = ipStr.lastIndexOf(":"); - if (lastIndexOf != -1) { - String portArea = ipStr.substring(lastIndexOf + 1); - try { - int parsedPort = Integer.parseInt(portArea); - if (parsedPort > 0 && parsedPort <= 65535) { - host = ipStr.substring(0, lastIndexOf); - port = parsedPort; - } - } catch (Exception ignore) { - } - } - return new ServerAddress(host, port); - } - - public InetSocketAddress toInetSocketAddress() { - return new InetSocketAddress(host, port); - } - - @Override - public @NotNull String toString() { - return host + ":" + port; - } - - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - ServerAddress that = (ServerAddress) o; - return port == that.port && Objects.equals(host, that.host); - } - - @Override - public int hashCode() { - return Objects.hash(host, port); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index a94c461e79a..0dafb77f4b8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -18,6 +18,8 @@ package org.jackhuang.hmcl.server; import com.google.gson.*; +import org.jackhuang.hmcl.server.resolver.ServerAddress; +import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; import org.jetbrains.annotations.NotNull; import java.io.*; @@ -38,36 +40,40 @@ private ServerStatusPinger() { } public static @NotNull ServerStatusResult getStatus(String serverIp) throws IOException { - return ServerStatusPinger.getStatus(ServerAddress.parseAddress(serverIp)); - } - - private static @NotNull ServerStatusResult getStatus(ServerAddress address) throws IOException { - ServerAddress resolvedAddress = ServerDnsSrvRedirector.lookup(address); - - try (Socket socket = new Socket()) { - socket.setOption(StandardSocketOptions.TCP_NODELAY, true); - socket.connect(resolvedAddress.toInetSocketAddress(), 7_000); - - try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); - DataInputStream in = new DataInputStream(socket.getInputStream())) { - - sendHandshakeStatusPacket(out, address.host(), address.port()); - sendStatusRequestPacket(out); - var status = readStatusResponsePacket(in); - - long sendTime = System.currentTimeMillis(); - sendPingRequestPacket(out); - var pong = readPongResponsePacket(in); - long networkLatency = System.currentTimeMillis() - sendTime; - - JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); - - ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); - return ServerStatusResult.success(parseServerStatus); + try { + ServerAddress address = ServerAddress.fromString(serverIp); + ServerAddressResolveResult resolveResult = address.resolve(); + if (resolveResult instanceof ServerAddressResolveResult.SuccessResult successResult) { + try (Socket socket = new Socket()) { + socket.setOption(StandardSocketOptions.TCP_NODELAY, true); + socket.connect(successResult.getConnectAddress(), 7_000); + + try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + DataInputStream in = new DataInputStream(socket.getInputStream())) { + + sendHandshakeStatusPacket(out, + successResult.getPackerHandshakeAddress(), successResult.getPackerHandshakePort() + ); + sendStatusRequestPacket(out); + var status = readStatusResponsePacket(in); + + long sendTime = System.currentTimeMillis(); + sendPingRequestPacket(out); + var pong = readPongResponsePacket(in); + long networkLatency = System.currentTimeMillis() - sendTime; + + JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); + + ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); + return ServerStatusResult.success(parseServerStatus); + } + } + } else { + return ServerStatusResult.failure(((ServerAddressResolveResult.FailureResult) resolveResult).getReason().getStatusReason()); } } catch (Exception e) { - LOG.error("Failed to get the status of server " + address, e); - return ServerStatusResult.failure(e); + LOG.error("Failed to get the status of server " + serverIp, e); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 4a810c975a3..5ee31b45413 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -24,20 +24,20 @@ public static ServerStatusResult success(ServerStatus serverStatus) { return new SuccessResult(serverStatus); } - public static ServerStatusResult failure(Exception exception) { - return new FailureResult(exception); + public static ServerStatusResult failure(FailureResult.Reason reason) { + return new FailureResult(reason); } - public @Nullable ServerStatus getIfSuccess() { + public @Nullable ServerStatus getIfSucceed() { if (this instanceof SuccessResult success) { return success.serverStatus; } return null; } - public @Nullable Exception exceptionIfFailure() { + public @Nullable ServerStatusResult.FailureResult.Reason getFailureReasonIfFailed() { if (this instanceof FailureResult failure) { - return failure.exception; + return failure.reason; } return null; } @@ -50,19 +50,25 @@ public boolean isFailure() { return this instanceof FailureResult; } - private static final class SuccessResult extends ServerStatusResult { + public static final class SuccessResult extends ServerStatusResult { public final ServerStatus serverStatus; - public SuccessResult(ServerStatus serverStatus) { + private SuccessResult(ServerStatus serverStatus) { this.serverStatus = serverStatus; } } - private static final class FailureResult extends ServerStatusResult { - public final Exception exception; + public static final class FailureResult extends ServerStatusResult { + public final Reason reason; - public FailureResult(Exception exception) { - this.exception = exception; + private FailureResult(Reason reason) { + this.reason = reason; + } + + public enum Reason { + EXCEPTION, + UNKNOWN_HOST, + BLOCKED_BY_MOJANG } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java new file mode 100644 index 00000000000..5ba41aacd1e --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java @@ -0,0 +1,38 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.resolver; + +import org.jetbrains.annotations.NotNull; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; + +public record HostAndIp( + @NotNull String host, + int port +) { + + @NotNull InetSocketAddress toInetSocketAddress() throws UnknownHostException { + return new InetSocketAddress(InetAddress.getByName(host), port); + } + + public boolean equals(@NotNull String host, int port) { + return host.equals(this.host) && port == this.port; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java new file mode 100644 index 00000000000..f6aa09b6dc7 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java @@ -0,0 +1,144 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.resolver; + +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public final class MojangBlockServerChecker { + private static volatile Predicate predicate; + + private MojangBlockServerChecker() { + + } + + static boolean isBlocking(InetSocketAddress inetSocketAddress0) { + if (predicate == null) { + synchronized (MojangBlockServerChecker.class) { + if (predicate == null) { + try { + URLConnection urlConnection = (new URL("https://sessionserver.mojang.com/blockedservers")).openConnection(); + urlConnection.setConnectTimeout(70_000); + urlConnection.setReadTimeout(70_000); + + try (InputStream is = urlConnection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1)) + ) { + Set collected = reader.lines().collect(Collectors.toUnmodifiableSet()); + + MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1"); + + predicate = new Predicate<>() { + private boolean isBlocking(@NotNull String server) { + if (server.startsWith("_minecraft._tcp.")) { + server = server.substring("_minecraft._tcp.".length()); + } + + while (server.charAt(server.length() - 1) == '.') { + server = server.substring(0, server.length() - 1); + } + + + if (isBlockedServerHostName(collected, server)) { + return true; + } else { + List parts = new ArrayList<>(Arrays.asList(server.split("\\."))); + boolean isIPV4 = isIPv4(parts); + if (!isIPV4 && isBlockedServerHostName(collected, "*." + server)) { + return true; + } + while (parts.size() > 1) { + parts.remove(isIPV4 ? parts.size() - 1 : 0); + String starredPart = isIPV4 ? String.join(".", parts) + ".*" : "*." + String.join(".", parts); + if (isBlockedServerHostName(collected, starredPart)) { + return true; + } + } + + return false; + } + } + + private boolean isIPv4(List address) { + if (address.size() != 4) { + return false; + } + + for (String s : address) { + try { + int part = Integer.parseInt(s); + if (part < 0 || part > 255) { + return false; + } + } catch (NumberFormatException ignore) { + return false; + } + } + return true; + } + + private boolean isBlockedServerHostName(@NotNull Set collect, String server) { + byte[] serverBytes = server.toLowerCase(Locale.ROOT).getBytes(StandardCharsets.ISO_8859_1); + byte[] hash; + synchronized (sha1MessageDigest) { + hash = sha1MessageDigest.digest(serverBytes); + } + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return collect.contains(hexString.toString()); + } + + @Override + public boolean test(InetSocketAddress inetSocketAddress) { + if (isBlocking(inetSocketAddress.getAddress().getHostName())) { + return true; + } + return isBlocking(inetSocketAddress.getAddress().getHostAddress()); + } + }; + } + + } catch (Exception e) { + LOG.error("Failed to initialize block server checker", e); + predicate = (hostAndIp) -> false; + } + } + } + } + return predicate.test(inetSocketAddress0); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java new file mode 100644 index 00000000000..024d6d1d63a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java @@ -0,0 +1,110 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.resolver; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class ServerAddress { + private final HostAndIp hostAndIp; + private final Map queryProperties; + + private ServerAddress( + HostAndIp hostAndIp, + Map queryProperties + ) { + this.hostAndIp = hostAndIp; + this.queryProperties = queryProperties; + } + + public static ServerAddress fromString(String input) throws IOException { + if (input == null || input.isEmpty()) throw new IOException("server ip is empty."); + + + Map queryProperties = new LinkedHashMap<>(); + + int queryStart = input.lastIndexOf('?'); + if (queryStart != -1) { + String queryPropertyStr = input.substring(queryStart + 1); + String[] args = queryPropertyStr.split("&"); + for (String arg : args) { + String[] pair = arg.split("="); + queryProperties.put( + URLDecoder.decode(pair[0], StandardCharsets.UTF_8), + URLDecoder.decode(pair[1], StandardCharsets.UTF_8) + ); + } + input = input.substring(0, queryStart); + } + + int atEnd = input.lastIndexOf('@'); + if (atEnd != -1) { + queryProperties.put("_id", input.substring(0, atEnd)); + input = input.substring(atEnd + 1); + } + + String host = input; + int port = 25565; + + int lastIndexOf = input.lastIndexOf(":"); + if (lastIndexOf != -1) { + String portArea = input.substring(lastIndexOf + 1); + try { + int parsedPort = Integer.parseInt(portArea); + if (parsedPort > 0 && parsedPort <= 65535) { + host = input.substring(0, lastIndexOf); + port = parsedPort; + } + } catch (Exception ignore) { + } + } + return new ServerAddress(new HostAndIp(host, port), queryProperties); + } + + public ServerAddressResolveResult resolve() { + try { + InetSocketAddress inetSocketAddress = hostAndIp.toInetSocketAddress(); + if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG); + } + HostAndIp lookupDnsResult = ServerDnsSrvRedirector.lookup(this); + if (lookupDnsResult != null) { + inetSocketAddress = lookupDnsResult.toInetSocketAddress(); + if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG); + } + } + return ServerAddressResolveResult.succeed(this, inetSocketAddress); + } catch (UnknownHostException e) { + return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST); + } + } + + public Map getQueryProperties() { + return queryProperties; + } + + public HostAndIp getHostAndIp() { + return hostAndIp; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java new file mode 100644 index 00000000000..924c5d7aa3a --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -0,0 +1,126 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.resolver; + +import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jetbrains.annotations.NotNull; + +import java.net.InetSocketAddress; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +public sealed class ServerAddressResolveResult { + + private ServerAddressResolveResult() { + + } + + public static @NotNull ServerAddressResolveResult.FailureResult failed(FailureResult.Reason reason) { + return new FailureResult(reason); + } + + public static SuccessResult succeed(ServerAddress serverAddress, InetSocketAddress address) { + Map queryProperties = new LinkedHashMap<>(serverAddress.getQueryProperties()); + boolean isSame = serverAddress.getHostAndIp().equals(address.getHostName(), address.getPort()); + + if (isSame && queryProperties.get("_o") != null) { + queryProperties.put("_o", serverAddress.getHostAndIp().host() + ":" + serverAddress.getHostAndIp().port()); + } + + StringBuilder queryArgBuilder = new StringBuilder(); + boolean first = true; + + for (Map.Entry entry : queryProperties.entrySet()) { + String key = entry.getKey(); + if (!first) { + queryArgBuilder.append("&"); + } + first = false; + + queryArgBuilder.append(URLEncoder.encode(key, StandardCharsets.UTF_8)); + queryArgBuilder.append("="); + queryArgBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + + if (queryArgBuilder.isEmpty()) { + queryArgBuilder.insert(0, address.getHostName()); + } else { + queryArgBuilder.insert(0, address.getHostName() + "?"); + } + return new SuccessResult(address, queryArgBuilder.toString(), address.getPort()); + } + + public static final class FailureResult extends ServerAddressResolveResult { + private final Reason reason; + + public FailureResult(Reason reason) { + this.reason = reason; + } + + public Reason getReason() { + return reason; + } + + public enum Reason { + UNKNOWN_HOST, + BLOCKED_BY_MOJANG; + + public ServerStatusResult.FailureResult.Reason getStatusReason() { + return switch (this) { + case UNKNOWN_HOST -> ServerStatusResult.FailureResult.Reason.UNKNOWN_HOST; + case BLOCKED_BY_MOJANG -> ServerStatusResult.FailureResult.Reason.BLOCKED_BY_MOJANG; + }; + } + } + } + + public static final class SuccessResult extends ServerAddressResolveResult { + private final InetSocketAddress connectAddress; + private final String packerHandshakeAddress; + private final int packerHandshakePort; + + public SuccessResult(InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { + this.connectAddress = connectAddress; + this.packerHandshakeAddress = packerHandshakeAddress; + this.packerHandshakePort = packerHandshakePort; + } + + public InetSocketAddress getConnectAddress() { + return connectAddress; + } + + public String getPackerHandshakeAddress() { + return packerHandshakeAddress; + } + + public int getPackerHandshakePort() { + return packerHandshakePort; + } + + @Override + public String toString() { + return "SuccessResult{" + + "connectAddress=" + connectAddress + + ", packerHandshakeAddress='" + packerHandshakeAddress + '\'' + + ", packerHandshakePort=" + packerHandshakePort + + '}'; + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java similarity index 62% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java index 70584a10613..e23636a5376 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnsSrvRedirector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java @@ -15,15 +15,15 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server; +package org.jackhuang.hmcl.server.resolver; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; -import java.io.IOException; import java.util.Hashtable; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -35,7 +35,7 @@ private ServerDnsSrvRedirector() { } - static @NotNull ServerAddress lookup(ServerAddress original) throws IOException { + static @Nullable HostAndIp lookup(ServerAddress address0) { if (redirector == null) { synchronized (ServerDnsSrvRedirector.class) { if (redirector == null) { @@ -49,30 +49,38 @@ private ServerDnsSrvRedirector() { DirContext context = new InitialDirContext(env); redirector = (address) -> { - if (address.port() != 25565) return address; - try { - Attributes attributes = context.getAttributes("_minecraft._tcp." + address.host(), new String[]{"SRV"}); - Attribute srvAttribute = attributes.get("srv"); - if (srvAttribute != null) { - String[] arguments = srvAttribute.get().toString().split(" ", 4); - return new ServerAddress(arguments[3], ServerAddress.parsePort(arguments[2])); + if (address.getHostAndIp().port() == 25565) { + try { + Attributes attributes = context.getAttributes("_minecraft._tcp." + address.getHostAndIp().host(), new String[]{"SRV"}); + Attribute srvAttribute = attributes.get("srv"); + if (srvAttribute != null) { + String[] arguments = srvAttribute.get().toString().split(" ", 4); + + int port = 25565; + try { + port = Integer.parseInt(arguments[2]); + } catch (Exception ignored) { + } + + return new HostAndIp(arguments[3], port); + } + } catch (Throwable ignored) { } - } catch (Throwable ignored) { } - return address; + return null; }; } catch (Throwable e) { LOG.error("Failed to initialize SRV redirect resolved, some servers might not work", e); - redirector = (address) -> address; + redirector = (address) -> null; } } } } - return redirector.lookup(original); + return redirector.lookup(address0); } @FunctionalInterface private interface DnsSrvRedirector { - @NotNull ServerAddress lookup(@NotNull ServerAddress original) throws IOException; + @Nullable HostAndIp lookup(@NotNull ServerAddress address); } } From 878e8fad7d00e472f8b46c84348e594a72559c16 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 09:10:15 +0800 Subject: [PATCH 33/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=BF=94=E5=9B=9E=E4=B8=8A=E4=B8=80?= =?UTF-8?q?=E6=AD=A5=E6=97=B6=E4=BC=9A=E5=B0=86=E5=BD=93=E5=89=8D=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E7=9A=84=E5=86=85=E5=AE=B9=E4=B8=A2=E5=BC=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/EditServerPane.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 660af5da23b..ccfc12fac8b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -65,10 +65,10 @@ public EditServerPane(Type type, ServerListPage.IconedServer reference, Consumer this.handleCallback = handleCallback; getStyleClass().add("skin-pane"); - initPaneContents(); + initPaneContents(reference.getName(), reference.getIp()); } - private void initPaneContents() { + private void initPaneContents(String serverName, String serverIP) { body.setDisable(false); body.getChildren().clear(); body.getColumnConstraints().clear(); @@ -112,8 +112,8 @@ private void initPaneContents() { })); body.add(txtServerName, 1, 0); - if (reference != null) { - txtServerName.setText(reference.getName()); + if (serverName != null) { + txtServerName.setText(serverName); } } @@ -128,8 +128,8 @@ private void initPaneContents() { setValidateWhileTextChanged(txtServerIP, true); body.add(txtServerIP, 1, 1); - if (reference != null) { - txtServerIP.setText(reference.getIp()); + if (serverIP != null) { + txtServerIP.setText(serverIP); } } @@ -203,7 +203,7 @@ private void onAdd() { } }); btnCancel.setOnAction(e -> { - initPaneContents(); + initPaneContents(serverName, serverIP); }); ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { From 74ed47f6057e91d0545fdc30d54dfe9b147cc051 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 09:40:22 +0800 Subject: [PATCH 34/97] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8ip=E5=8C=85=E5=90=AB=E7=9A=84query=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=9C=A8=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=88=97=E8=A1=A8=E4=B8=AD?= =?UTF-8?q?=E9=9A=90=E8=97=8F=EF=BC=8C=E5=9C=A8=E7=8A=B6=E6=80=81=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=B8=AD=E6=98=BE=E7=A4=BA=E4=B8=BA=E5=8D=8A=E9=80=8F?= =?UTF-8?q?=E6=98=8E=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/ServerAddressMaskHBox.java | 54 +++++++++++++++++++ .../ui/instances/server/ServerListPage.java | 11 +++- .../ui/instances/server/ServerStatusPane.java | 2 +- 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java new file mode 100644 index 00000000000..ee2e1f8408d --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java @@ -0,0 +1,54 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.instances.server; + +import javafx.scene.control.Label; +import javafx.scene.layout.HBox; +import org.jetbrains.annotations.Nullable; + + +public class ServerAddressMaskHBox extends HBox { + public final Label realServerLabel = new Label(); + public final Label queryLabel = new Label(); + + public ServerAddressMaskHBox(@Nullable String serverIP) { + queryLabel.setOpacity(0.3); + + getChildren().add(realServerLabel); + getChildren().add(queryLabel); + set(serverIP); + } + + public void set(@Nullable String serverIP) { + if (serverIP == null) { + realServerLabel.setText(""); + queryLabel.setText(""); + return; + } + int queryStart = serverIP.lastIndexOf('?'); + if (queryStart != -1) { + String queryPropertyStr = serverIP.substring(queryStart); + queryLabel.setText(queryPropertyStr); + serverIP = serverIP.substring(0, queryStart); + } else { + queryLabel.setText(""); + } + + realServerLabel.setText(serverIP); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index fc5f67b8894..ede07f10914 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -383,7 +383,16 @@ protected void updateItem(ServerHolder holder, boolean empty) { } else { serverIcon.setImage(holder.server.iconImage); content.setTitle(holder.server.getName() != null ? parseColorEscapes(holder.server.getName()) : ""); - content.setSubtitle(holder.server.getIp()); + + String displayIp = holder.server.getIp(); + if (displayIp == null) displayIp = ""; + int queryStart = holder.server.getIp().lastIndexOf('?'); + boolean mask = false; + if (queryStart != -1) { + mask = true; + displayIp = displayIp.substring(0, queryStart); + } + content.setSubtitle(displayIp); if (holder.server.isHidden()) { content.addTag(i18n("server.tag.hide")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 866a6c4576b..3c656fc0349 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -222,7 +222,7 @@ public void applyResult(ServerStatusResult statusResult) { textPane.add(new Label(iconedServer.getName()), 1, 0); textPane.add(new Label(i18n("server.ip") + ":"), 0, 1); - textPane.add(new Label(iconedServer.getIp()), 1, 1); + textPane.add(new ServerAddressMaskHBox(iconedServer.getIp()), 1, 1); root.getChildren().add(serverInfoBox); if (status != null) { From 438f1a89a1f7212a531e1d84f50368457b55111e Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 09:44:30 +0800 Subject: [PATCH 35/97] Update ServerAddressMaskHBox --- .../hmcl/ui/instances/server/ServerAddressMaskHBox.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java index ee2e1f8408d..26fe7703bd7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java @@ -21,7 +21,6 @@ import javafx.scene.layout.HBox; import org.jetbrains.annotations.Nullable; - public class ServerAddressMaskHBox extends HBox { public final Label realServerLabel = new Label(); public final Label queryLabel = new Label(); From 2fb33bff125dd1292a9cbbf593ed7f7f898f7cb5 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 10:01:15 +0800 Subject: [PATCH 36/97] =?UTF-8?q?=E4=B8=80=E4=BA=9B=E7=BB=86=E8=8A=82?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E4=B8=8A=E7=9A=84=E9=97=AE=E9=A2=98=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/EditServerPane.java | 9 +++++++-- .../hmcl/ui/instances/server/ServerListPage.java | 6 ++---- .../org/jackhuang/hmcl/server/ServerStatusPinger.java | 1 + .../hmcl/server/resolver/MojangBlockServerChecker.java | 4 ++-- .../hmcl/server/resolver/ServerAddressResolveResult.java | 2 +- .../src/main/java/org/jackhuang/hmcl/util/NBTUtils.java | 8 ++++---- 6 files changed, 17 insertions(+), 13 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index ccfc12fac8b..6d9cb4331fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -37,6 +37,7 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.RequiredValidator; import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jetbrains.annotations.Nullable; import java.util.Objects; import java.util.function.Consumer; @@ -59,13 +60,17 @@ public class EditServerPane extends TransitionPane implements DialogAware { private final SpinnerPane spinner = new SpinnerPane(); private final Label lblErrorMessage = new Label(); - public EditServerPane(Type type, ServerListPage.IconedServer reference, Consumer handleCallback) { + public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference, Consumer handleCallback) { this.type = type; this.reference = reference; this.handleCallback = handleCallback; getStyleClass().add("skin-pane"); - initPaneContents(reference.getName(), reference.getIp()); + if (reference != null) { + initPaneContents(reference.getName(), reference.getIp()); + } else { + initPaneContents("", ""); + } } private void initPaneContents(String serverName, String serverIP) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index ede07f10914..a2b4869ef32 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -357,6 +357,7 @@ protected void updateItem(ServerHolder holder, boolean empty) { boolean oldEmpty = isEmpty(); super.updateItem(holder, empty); + if (oldHolder == holder && oldEmpty == empty) return; if (serverStatusPingingValueSubscription != null) { serverStatusPingingValueSubscription.unsubscribe(); @@ -367,9 +368,6 @@ protected void updateItem(ServerHolder holder, boolean empty) { serverStatusResultValueSubscription = null; } - - if (oldHolder == holder && oldEmpty == empty) return; - this.graphic.releaseRippleImmediately(); this.content.getTags().clear(); @@ -386,7 +384,7 @@ protected void updateItem(ServerHolder holder, boolean empty) { String displayIp = holder.server.getIp(); if (displayIp == null) displayIp = ""; - int queryStart = holder.server.getIp().lastIndexOf('?'); + int queryStart = displayIp.lastIndexOf('?'); boolean mask = false; if (queryStart != -1) { mask = true; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index 0dafb77f4b8..af002828b5f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -47,6 +47,7 @@ private ServerStatusPinger() { try (Socket socket = new Socket()) { socket.setOption(StandardSocketOptions.TCP_NODELAY, true); socket.connect(successResult.getConnectAddress(), 7_000); + socket.setSoTimeout(7_000); try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); DataInputStream in = new DataInputStream(socket.getInputStream())) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java index f6aa09b6dc7..9ed8f0b37f3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java @@ -46,8 +46,8 @@ static boolean isBlocking(InetSocketAddress inetSocketAddress0) { if (predicate == null) { try { URLConnection urlConnection = (new URL("https://sessionserver.mojang.com/blockedservers")).openConnection(); - urlConnection.setConnectTimeout(70_000); - urlConnection.setReadTimeout(70_000); + urlConnection.setConnectTimeout(7_000); + urlConnection.setReadTimeout(7_000); try (InputStream is = urlConnection.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java index 924c5d7aa3a..93bd8114da2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -40,7 +40,7 @@ public static SuccessResult succeed(ServerAddress serverAddress, InetSocketAddre Map queryProperties = new LinkedHashMap<>(serverAddress.getQueryProperties()); boolean isSame = serverAddress.getHostAndIp().equals(address.getHostName(), address.getPort()); - if (isSame && queryProperties.get("_o") != null) { + if (!isSame || queryProperties.get("_o") != null) { queryProperties.put("_o", serverAddress.getHostAndIp().host() + ":" + serverAddress.getHostAndIp().port()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java index 2fa268b58eb..2ff76b8ea2f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java @@ -19,10 +19,10 @@ import org.glavo.nbt.io.NBTCodec; import org.glavo.nbt.tag.*; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; import java.util.Optional; @@ -36,9 +36,9 @@ public static CompoundTag readCompressed(Path file) throws IOException { } public static void writeCompressed(CompoundTag tag, Path file) throws IOException { - try (var output = Files.newOutputStream(file)) { - NBTCodec.of().writeTag(output, tag); - } + FileUtils.saveSafely(file, outputStream -> + NBTCodec.of().writeTag(outputStream, tag) + ); } public static OptionalLong tryGetLong(Tag tag) { From 08bff98d8f387e6677270a03c8fd02a5db929578 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 17:12:32 +0800 Subject: [PATCH 37/97] =?UTF-8?q?=E5=B0=86=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E5=9C=B0=E5=9D=80@=E5=89=8D=E4=BB=A5=E5=8F=8A=3F=E5=90=8E?= =?UTF-8?q?=E7=9A=84=E5=86=85=E5=AE=B9=E8=AE=BE=E7=BD=AE=E4=B8=BA=E6=8E=A9?= =?UTF-8?q?=E7=A0=81=EF=BC=8C=E5=9C=A8=E7=8A=B6=E6=80=81=E7=95=8C=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=E6=82=AC=E6=B5=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...skHBox.java => ServerAddressMaskPane.java} | 49 ++++++++++++++++--- .../ui/instances/server/ServerListPage.java | 43 ++++++++-------- .../ui/instances/server/ServerStatusPane.java | 2 +- 3 files changed, 65 insertions(+), 29 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/{ServerAddressMaskHBox.java => ServerAddressMaskPane.java} (50%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java similarity index 50% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java index 26fe7703bd7..0d3d76bb900 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskHBox.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java @@ -19,33 +19,70 @@ import javafx.scene.control.Label; import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class ServerAddressMaskHBox extends HBox { +public class ServerAddressMaskPane extends HBox { + public final Label atLabel = new Label(); public final Label realServerLabel = new Label(); public final Label queryLabel = new Label(); - public ServerAddressMaskHBox(@Nullable String serverIP) { + public ServerAddressMaskPane(@Nullable String serverIP) { queryLabel.setOpacity(0.3); + atLabel.setOpacity(0.3); + queryLabel.setMinWidth(Region.USE_PREF_SIZE); + atLabel.setMinWidth(Region.USE_PREF_SIZE); + realServerLabel.setMinWidth(Region.USE_PREF_SIZE); + + getChildren().add(atLabel); getChildren().add(realServerLabel); getChildren().add(queryLabel); set(serverIP); } + public static void installMask(@NotNull Label label, String text) { + int length = text.length(); + StringBuilder maskBuilder = new StringBuilder(); + for (int i = 0; i < length; i++) { + char c = text.charAt(i); + if (c == '@' || c == '?') { + maskBuilder.append(c); + } else { + maskBuilder.append("*"); + } + } + String mask = maskBuilder.toString(); + + label.setText(mask); + + label.hoverProperty().addListener((obs, wasHover, isHover) -> { + label.setText(isHover ? text : mask); + }); + } + public void set(@Nullable String serverIP) { if (serverIP == null) { realServerLabel.setText(""); - queryLabel.setText(""); + installMask(atLabel, ""); + installMask(queryLabel, ""); return; } int queryStart = serverIP.lastIndexOf('?'); if (queryStart != -1) { - String queryPropertyStr = serverIP.substring(queryStart); - queryLabel.setText(queryPropertyStr); + installMask(queryLabel, serverIP.substring(queryStart)); serverIP = serverIP.substring(0, queryStart); } else { - queryLabel.setText(""); + installMask(queryLabel, ""); + } + + int atEnd = serverIP.indexOf('@'); + if (atEnd != -1) { + installMask(atLabel, serverIP.substring(0, atEnd + 1)); + serverIP = serverIP.substring(atEnd + 1); + } else { + installMask(atLabel, ""); } realServerLabel.setText(serverIP); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index a2b4869ef32..6cacc91e0ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -36,6 +36,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.util.Subscription; import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.game.HMCLGameInstance; @@ -256,7 +257,8 @@ private static final class ServerListCell extends ListCell !e.equals(page.gameInstance)) .map(gameInstance -> gameInstance.getId().id()) - .forEach(content::addTag); + .forEach(contentLine1::addTag); } else { holder.holdInstances.stream() .map(gameInstance -> gameInstance.getId().id()) - .forEach(content::addTag); + .forEach(contentLine1::addTag); } setGraphic(graphic); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 3c656fc0349..8bd0ae5de31 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -222,7 +222,7 @@ public void applyResult(ServerStatusResult statusResult) { textPane.add(new Label(iconedServer.getName()), 1, 0); textPane.add(new Label(i18n("server.ip") + ":"), 0, 1); - textPane.add(new ServerAddressMaskHBox(iconedServer.getIp()), 1, 1); + textPane.add(new ServerAddressMaskPane(iconedServer.getIp()), 1, 1); root.getChildren().add(serverInfoBox); if (status != null) { From ec6d18fbb30d77ae9b5af27220ee1b0d99d27e19 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 18:01:13 +0800 Subject: [PATCH 38/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E7=9A=84=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/instances/server/EditServerPane.java | 3 +-- .../org/jackhuang/hmcl/ui/instances/server/ServerListPage.java | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 6d9cb4331fb..fb3ee9792ff 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -176,7 +176,6 @@ protected boolean computeValue() { } private void onAdd() { - spinner.showSpinner(); String serverName; String serverIP = txtServerIP.getText(); if (serverIP == null) return; @@ -187,7 +186,7 @@ private void onAdd() { } body.setDisable(true); - + spinner.showSpinner(); Task.supplyAsync(Schedulers.io(), () -> ServerStatusPinger.getStatus(serverIP) ).whenComplete(Schedulers.javafx(), (result, ignored) -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 6cacc91e0ff..171e5980414 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -492,7 +492,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos ) ); if (page.gameInstance != null) { - copyToInstanceMEnuItem.setDisable(getItem().fromServersDatFilePath.equals(page.gameInstance.getServersDatFilePath())); + copyToInstanceMEnuItem.setDisable(holder.fromServersDatFilePath.equals(page.gameInstance.getServersDatFilePath())); } JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup); From c4aaa256ec8577307fc4c057c573debde74a3aeb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 18:18:10 +0800 Subject: [PATCH 39/97] =?UTF-8?q?=E5=88=97=E8=A1=A8=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E5=9C=B0=E5=9D=80=E6=A0=87=E7=AD=BE=E5=BA=94=E7=94=A8?= =?UTF-8?q?subtitle=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerAddressMaskPane.java | 6 ++++++ .../jackhuang/hmcl/ui/instances/server/ServerListPage.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java index 0d3d76bb900..aaeb42f8ffe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java @@ -87,4 +87,10 @@ public void set(@Nullable String serverIP) { realServerLabel.setText(serverIP); } + + public void labelAddStyleClass(String styleClass) { + atLabel.getStyleClass().add(styleClass); + realServerLabel.getStyleClass().add(styleClass); + queryLabel.getStyleClass().add(styleClass); + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 171e5980414..59b7cbfb9e1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -287,7 +287,7 @@ public ServerListCell(ServerListPage page) { { this.contentLine1 = new TwoLineListItem(); this.contentLine2AddressMaskPane = new ServerAddressMaskPane(""); - contentLine2AddressMaskPane.getStyleClass().add("subtitle"); + contentLine2AddressMaskPane.labelAddStyleClass("subtitle"); HBox contentLine2 = new HBox(contentLine2AddressMaskPane); VBox center = new VBox(contentLine1, contentLine2); From 1ded58c0f8c1558e9580fa9ad0a8f3e6f1a33762 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 19:02:33 +0800 Subject: [PATCH 40/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20acceptTextures=20?= =?UTF-8?q?=E4=B8=8E=E5=8E=9F=E7=89=88=E8=A1=8C=E4=B8=BA=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 5 +-- .../ui/instances/server/ServerListPage.java | 22 ++++++------- .../org/jackhuang/hmcl/server/Server.java | 31 +++++++++++++------ .../org/jackhuang/hmcl/server/ServerDnD.java | 2 +- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index fb3ee9792ff..4dee99bee23 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -27,6 +27,7 @@ import javafx.scene.control.Label; import javafx.scene.control.TextFormatter; import javafx.scene.layout.*; +import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusPinger; import org.jackhuang.hmcl.server.ServerStatusResult; @@ -203,7 +204,7 @@ private void onAdd() { if (reference != null) { handleCallback.accept(reference.withIpAndName(serverIP, serverName)); } else { - handleCallback.accept(new ServerListPage.IconedServer(false, false, null, serverIP, serverName)); + handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIP, serverName)); } }); btnCancel.setOnAction(e -> { @@ -220,7 +221,7 @@ private void onAdd() { if (reference != null) { handleCallback.accept(reference.withIpAndName(serverIP, serverName)); } else { - handleCallback.accept(new ServerListPage.IconedServer(false, false, status.favicon(), serverIP, serverName)); + handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, status.favicon(), serverIP, serverName)); } } }).start(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 59b7cbfb9e1..01cb0ca94f1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -503,26 +503,18 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos public static class IconedServer extends Server { final Image iconImage; - public IconedServer(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { - super(acceptTextures, hidden, icon, ip, name); + public IconedServer(ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { + super(serverPackStatus, hidden, icon, ip, name); iconImage = parseImageOrDefault(icon); } - public IconedServer withIcon(@Nullable String newIcon) { - return new IconedServer(isAcceptTextures(), isHidden(), newIcon, getIp(), getName()); - } - - public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { - return new IconedServer(isAcceptTextures(), isHidden(), getIcon(), newIp, newName); - } - public static IconedServer pack(Server server) { if (server instanceof IconedServer) { return (IconedServer) server; } return new IconedServer( - server.isAcceptTextures(), + server.getServerPackStatus(), server.isHidden(), server.getIcon(), server.getIp(), @@ -530,6 +522,14 @@ public static IconedServer pack(Server server) { ); } + public IconedServer withIcon(@Nullable String newIcon) { + return new IconedServer(getServerPackStatus(), isHidden(), newIcon, getIp(), getName()); + } + + public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { + return new IconedServer(getServerPackStatus(), isHidden(), getIcon(), newIp, newName); + } + public static @NotNull Image parseImageOrDefault(@Nullable String imageBase64String) { if (imageBase64String != null && !imageBase64String.isEmpty()) { try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 77e2faf5f0b..9d47c44d763 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -31,14 +31,14 @@ import static org.jackhuang.hmcl.util.NBTUtils.writeCompressed; public class Server { - private final boolean acceptTextures; + private final @NotNull ServerPackStatus serverPackStatus; private final boolean hidden; private final @Nullable String icon; private final @Nullable String ip; private final @Nullable String name; - public Server(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { - this.acceptTextures = acceptTextures; + public Server(@NotNull ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { + this.serverPackStatus = serverPackStatus; this.hidden = hidden; this.icon = icon; this.ip = ip; @@ -47,7 +47,7 @@ public Server(boolean acceptTextures, boolean hidden, @Nullable String icon, @Nu public static Server loadSingle(@NotNull CompoundTag tag) { return new Server( - tag.get("acceptTextures") instanceof ByteTag bt && bt.getValue() != 0, + tag.get("acceptTextures") instanceof ByteTag bt ? (bt.getValue() == 0 ? ServerPackStatus.DISABLED : ServerPackStatus.ENABLED) : ServerPackStatus.PROMPT, tag.get("hidden") instanceof ByteTag bt && bt.getValue() != 0, tag.get("icon") instanceof StringTag stg ? stg.getValue() : null, tag.get("ip") instanceof StringTag stg ? stg.getValue() : null, @@ -91,7 +91,12 @@ public static void saveToServersDat(@NotNull List servers, @NotNull Path } public void writeToCompoundTag(CompoundTag tag) { - tag.addByte("acceptTextures", (byte) (acceptTextures ? 1 : 0)); + switch (serverPackStatus) { + case ENABLED -> tag.addByte("acceptTextures", (byte) 1); + case DISABLED -> tag.addByte("acceptTextures", (byte) 0); + case PROMPT -> { + } + } tag.addByte("hidden", (byte) (hidden ? 1 : 0)); if (icon != null) tag.addString("icon", icon); if (ip != null) tag.addString("ip", ip); @@ -102,16 +107,16 @@ public void writeToCompoundTag(CompoundTag tag) { public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; Server server = (Server) o; - return acceptTextures == server.acceptTextures && hidden == server.hidden && Objects.equals(icon, server.icon) && Objects.equals(ip, server.ip) && Objects.equals(name, server.name); + return serverPackStatus == server.serverPackStatus && hidden == server.hidden && Objects.equals(icon, server.icon) && Objects.equals(ip, server.ip) && Objects.equals(name, server.name); } @Override public int hashCode() { - return Objects.hash(acceptTextures, hidden, icon, ip, name); + return Objects.hash(serverPackStatus, hidden, icon, ip, name); } - public boolean isAcceptTextures() { - return acceptTextures; + public ServerPackStatus getServerPackStatus() { + return serverPackStatus; } public boolean isHidden() { @@ -133,11 +138,17 @@ public boolean isHidden() { @Override public String toString() { return "Server{" + - "acceptTextures=" + acceptTextures + + "acceptTextures=" + serverPackStatus + ", hidden=" + hidden + ", icon='" + icon + '\'' + ", ip='" + ip + '\'' + ", name='" + name + '\'' + '}'; } + + public enum ServerPackStatus { + ENABLED(), + DISABLED(), + PROMPT(); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java index 5f09ec084ec..f6719ce7eaf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -48,7 +48,7 @@ public static Optional parseUrlFromDragboard(Dragboard dragboard) { try { return Optional.of(new Server( - false, + Server.ServerPackStatus.PROMPT, false, urlElements.length == 4 ? urlElements[3] : null, new String(Base64.getDecoder().decode(urlElements[2]), StandardCharsets.UTF_8), From a16214a1afc84385dca89d032d14168db6239651 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 1 Oct 2026 22:22:12 +0800 Subject: [PATCH 41/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20ServerAddress.fromSt?= =?UTF-8?q?ring=20=E5=B0=BD=E9=87=8F=E4=B8=8E=E5=8E=9F=E7=89=88=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA=E4=BF=9D=E6=8C=81=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/server/resolver/ServerAddress.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java index 024d6d1d63a..44964c192a8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java @@ -48,11 +48,18 @@ public static ServerAddress fromString(String input) throws IOException { String queryPropertyStr = input.substring(queryStart + 1); String[] args = queryPropertyStr.split("&"); for (String arg : args) { - String[] pair = arg.split("="); - queryProperties.put( - URLDecoder.decode(pair[0], StandardCharsets.UTF_8), - URLDecoder.decode(pair[1], StandardCharsets.UTF_8) - ); + int separator = arg.indexOf('='); + + String key; + String value; + if (separator >= 0) { + key = URLDecoder.decode(arg.substring(0, separator), StandardCharsets.UTF_8); + value = URLDecoder.decode(arg.substring(separator + 1), StandardCharsets.UTF_8); + } else { + key = URLDecoder.decode(arg, StandardCharsets.UTF_8); + value = ""; + } + queryProperties.put(key, value); } input = input.substring(0, queryStart); } From fc2974719224b788e87c145089fe3eddc1236314 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 2 Oct 2026 17:37:01 +0800 Subject: [PATCH 42/97] =?UTF-8?q?=E9=99=90=E5=88=B6ping=E5=8C=85=E6=8E=A5?= =?UTF-8?q?=E6=94=B6=E7=9A=84=E5=A4=A7=E5=B0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/server/ServerStatusPinger.java | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index af002828b5f..37a21e7229f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -148,30 +148,34 @@ private static ServerStatus parseServerStatus(long networkLatency, JsonObject ro } private static long readPongResponsePacket(DataInputStream in) throws IOException { - byte[] packetData = new byte[readVarInt(in)]; - in.readFully(packetData); - - try (ByteArrayInputStream bais = new ByteArrayInputStream(packetData); - DataInputStream packetIn = new DataInputStream(bais)) { + return readPacket(in, packetIn -> { int packetId = readVarInt(packetIn); if (packetId != 0x01) throw new IOException("Invalid packet id " + packetId + ", expected 0x01(Pong Response)."); return readLong(packetIn); - } + }); } - private static String readStatusResponsePacket(DataInputStream in) throws IOException { - byte[] packetData = new byte[readVarInt(in)]; + private static T readPacket(DataInputStream in, PacketReader reader) throws IOException { + int length = readVarInt(in); + if (length > 32768) throw new IOException("Packet length too large: " + length); + byte[] packetData = new byte[length]; in.readFully(packetData); try (ByteArrayInputStream bais = new ByteArrayInputStream(packetData); DataInputStream packetIn = new DataInputStream(bais)) { + return reader.reader(packetIn); + } + } + + private static String readStatusResponsePacket(DataInputStream in) throws IOException { + return readPacket(in, packetIn -> { int packetId = readVarInt(packetIn); if (packetId != 0x00) throw new IOException("Invalid packet id " + packetId + ", expected 0x00(Status Response)."); return readVarString(packetIn); // Status Json - } + }); } private static void sendStatusRequestPacket(DataOutputStream sendTarget) throws IOException { @@ -229,7 +233,9 @@ private static void writeVarInt(OutputStream out, int value) throws IOException } private static String readVarString(DataInputStream in) throws IOException { - byte[] bytes = new byte[readVarInt(in)]; + int length = readVarInt(in); + if (length > 32768) throw new IOException("Packet string length too large: " + length); + byte[] bytes = new byte[length]; in.readFully(bytes); return new String(bytes, StandardCharsets.UTF_8); } @@ -283,4 +289,9 @@ private static long readLong(InputStream in) throws IOException { private interface PacketWriter { void write(DataOutputStream out) throws IOException; } + + @FunctionalInterface + private interface PacketReader { + T reader(DataInputStream in) throws IOException; + } } From 6a263279c8b1a60687f3d6a8500c37759a71cd89 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 2 Oct 2026 18:01:30 +0800 Subject: [PATCH 43/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E6=9B=B4=E6=96=B0servers.dat=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E9=81=BF=E5=85=8D=E5=B9=B6=E5=8F=91=E5=86=99=E5=85=A5=E4=BA=92?= =?UTF-8?q?=E7=9B=B8=E8=A6=86=E7=9B=96=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 01cb0ca94f1..0749f51dda2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -52,6 +52,7 @@ import org.jetbrains.annotations.Nullable; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; @@ -149,6 +150,7 @@ public void copyToInstance(ServerHolder holder) { } public void addServer(Server server) { + HMCLGameInstance gameInstance = this.gameInstance; if (gameInstance == null) return; Task.runAsync(Schedulers.io(), () -> { @@ -221,13 +223,17 @@ private void refresh() { pathListMap.put(instance.getServersDatFilePath(), holders); if (Files.exists(instance.getServersDatFilePath())) { - List parsedServers = Server.loadFromServersDat(instance.getServersDatFilePath()); - for (int index = 0; index < parsedServers.size(); index++) { - Server server = parsedServers.get(index); - ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), parsedServers, index, IconedServer.pack(server)); - holder.holdInstances.add(instance); - - holders.add(holder); + try { + List parsedServers = Server.loadFromServersDat(instance.getServersDatFilePath()); + for (int index = 0; index < parsedServers.size(); index++) { + Server server = parsedServers.get(index); + ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), parsedServers, index, IconedServer.pack(server)); + holder.holdInstances.add(instance); + + holders.add(holder); + } + } catch (IOException e) { + LOG.error("Failed to load servers from dat file: " + instance.getServersDatFilePath(), e); } } } @@ -445,18 +451,18 @@ private void applyServerStatusResult(ServerHolder holder, ServerStatusResult res statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); // update latest server icon serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); - if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { - // save latest icon - Task.runAsync(Schedulers.io(), () -> { - try { - IconedServer newServer = holder.server.withIcon(serverStatus.favicon()); - holder.cachedServers.set(holder.inDatPathSlot, newServer); - Server.saveToServersDat(holder.cachedServers, holder.fromServersDatFilePath); - } catch (Exception e) { - LOG.error("Failed to save servers dat", e); - } - }).start(); - } +// if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { +// // save latest icon +// Task.runAsync(Schedulers.io(), () -> { +// try { +// IconedServer newServer = holder.server.withIcon(serverStatus.favicon()); +// holder.cachedServers.set(holder.inDatPathSlot, newServer); +// Server.saveToServersDat(holder.cachedServers, holder.fromServersDatFilePath); +// } catch (Exception e) { +// LOG.error("Failed to save servers dat", e); +// } +// }).start(); +// } } } From cd85d1a56b82c5fb6443711c0295bf5df379aad4 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 3 Oct 2026 20:06:44 +0800 Subject: [PATCH 44/97] =?UTF-8?q?=E8=B0=83=E6=95=B4=20Server=20DnD=20Forma?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/server/ServerDnD.java | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java index f6719ce7eaf..4de289faa8f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.server; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import javafx.event.EventHandler; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; @@ -27,7 +29,38 @@ import java.util.Optional; import java.util.function.Consumer; -// minecraft-server:base64ServerName:base64ServerIP[:base64Favicon] +/** + * Drag-and-drop payload for a Minecraft server definition. + *

+ * Format: {@code minecraft-server:Base64(JSON)} + * + *

+ * Required fields: + *

    + *
  • {@code name}: the display name of the server.
  • + *
  • {@code address}: the server address, such as + * "...@server.ip.example.com:25565?_id=xxx&key=value...".
  • + *
+ * + *

+ * Optional fields: + *

    + *
  • {@code favicon}: the server icon. This value should be the raw PNG base64 payload only, without + * the {@code data:image/png;base64,} prefix. The parser in {@link ServerStatusPinger} accepts the + * Minecraft status format where the remote payload may include a data URL, but it strips the prefix and + * keeps only the base64 bytes. This drag-and-drop payload therefore stores the base64 text directly, + * not the full data URL.
  • + *
+ * + * Example payload: + *
+ * {
+ *   "name": "server name",
+ *   "address": "...@server.ip.example.com:25565?_id=xxx&key=value...",
+ *   "favicon": "iVBORw0KGgoAAAANSUhEUgAA..."
+ * }
+ * 
+ */ public final class ServerDnD { private static final String SCHEME = "minecraft-server"; @@ -38,21 +71,22 @@ public static Optional parseUrlFromDragboard(Dragboard dragboard) { String url = dragboard.getString(); if (url == null) return Optional.empty(); - - String[] urlElements = url.split(":"); - - if (urlElements.length != 4 && urlElements.length != 3) { - return Optional.empty(); - } - if (!urlElements[0].equals(SCHEME)) return Optional.empty(); - + int index = url.indexOf(":"); + if (index == -1) return Optional.empty(); try { + JsonObject object = JsonParser.parseString(new String(Base64.getDecoder().decode(url.substring(index + 1)), StandardCharsets.UTF_8)).getAsJsonObject(); + String favicon = null; + if (object.has("favicon")) { + favicon = object.get("favicon").getAsString(); + } + String address = object.get("address").getAsString(); + String name = object.get("name").getAsString(); return Optional.of(new Server( Server.ServerPackStatus.PROMPT, false, - urlElements.length == 4 ? urlElements[3] : null, - new String(Base64.getDecoder().decode(urlElements[2]), StandardCharsets.UTF_8), - new String(Base64.getDecoder().decode(urlElements[1]), StandardCharsets.UTF_8) + favicon, + address, + name )); } catch (Exception ignored) { } From 3183b461a534127e307a60eb8d7736cdddb5e19c Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 17:17:14 +0800 Subject: [PATCH 45/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Server=20DnD=20?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=88=A4=E6=96=AD=20SCHEME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java index 4de289faa8f..a9401aecc93 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -73,6 +73,7 @@ public static Optional parseUrlFromDragboard(Dragboard dragboard) { int index = url.indexOf(":"); if (index == -1) return Optional.empty(); + if (!SCHEME.equals(url.substring(0, index))) return Optional.empty(); try { JsonObject object = JsonParser.parseString(new String(Base64.getDecoder().decode(url.substring(index + 1)), StandardCharsets.UTF_8)).getAsJsonObject(); String favicon = null; From c4cd0a6ce87177e2b46161cac5e8d81219ec03f8 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 17:25:20 +0800 Subject: [PATCH 46/97] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E6=8E=A9=E7=A0=81=E9=83=A8=E5=88=86=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E4=B8=BA=E6=82=AC=E6=B5=AE=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/ServerAddressMaskPane.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java index aaeb42f8ffe..ad82443813f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java @@ -18,15 +18,20 @@ package org.jackhuang.hmcl.ui.instances.server; import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import org.jackhuang.hmcl.ui.FXUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class ServerAddressMaskPane extends HBox { - public final Label atLabel = new Label(); - public final Label realServerLabel = new Label(); - public final Label queryLabel = new Label(); + private final Label atLabel = new Label(); + private final Label realServerLabel = new Label(); + private final Label queryLabel = new Label(); + + private final Tooltip atLabelTooltip = new Tooltip(); + private final Tooltip queryLabelTooltip = new Tooltip(); public ServerAddressMaskPane(@Nullable String serverIP) { queryLabel.setOpacity(0.3); @@ -39,10 +44,13 @@ public ServerAddressMaskPane(@Nullable String serverIP) { getChildren().add(atLabel); getChildren().add(realServerLabel); getChildren().add(queryLabel); + + FXUtils.installFastTooltip(atLabel, atLabelTooltip); + FXUtils.installFastTooltip(queryLabel, queryLabelTooltip); set(serverIP); } - public static void installMask(@NotNull Label label, String text) { + public static void installMask(@NotNull Label label, Tooltip tooltip, String text) { int length = text.length(); StringBuilder maskBuilder = new StringBuilder(); for (int i = 0; i < length; i++) { @@ -56,33 +64,30 @@ public static void installMask(@NotNull Label label, String text) { String mask = maskBuilder.toString(); label.setText(mask); - - label.hoverProperty().addListener((obs, wasHover, isHover) -> { - label.setText(isHover ? text : mask); - }); + tooltip.setText(text); } public void set(@Nullable String serverIP) { if (serverIP == null) { realServerLabel.setText(""); - installMask(atLabel, ""); - installMask(queryLabel, ""); + installMask(atLabel, atLabelTooltip, ""); + installMask(queryLabel, queryLabelTooltip, ""); return; } int queryStart = serverIP.lastIndexOf('?'); if (queryStart != -1) { - installMask(queryLabel, serverIP.substring(queryStart)); + installMask(queryLabel, queryLabelTooltip, serverIP.substring(queryStart)); serverIP = serverIP.substring(0, queryStart); } else { - installMask(queryLabel, ""); + installMask(queryLabel, queryLabelTooltip, ""); } int atEnd = serverIP.indexOf('@'); if (atEnd != -1) { - installMask(atLabel, serverIP.substring(0, atEnd + 1)); + installMask(atLabel, atLabelTooltip, serverIP.substring(0, atEnd + 1)); serverIP = serverIP.substring(atEnd + 1); } else { - installMask(atLabel, ""); + installMask(atLabel, atLabelTooltip, ""); } realServerLabel.setText(serverIP); From 202d06c0b8700e0125a8ab99948563216aa8f81a Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 17:53:47 +0800 Subject: [PATCH 47/97] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=B0=83=E6=95=B4=20Ed?= =?UTF-8?q?itServerPane=20=E7=95=8C=E9=9D=A2=E6=93=8D=E4=BD=9C=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 133 +++++++++++------- 1 file changed, 84 insertions(+), 49 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 4dee99bee23..c7a750be34a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -21,6 +21,8 @@ import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXTextField; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.ChangeListener; import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; @@ -53,13 +55,15 @@ public class EditServerPane extends TransitionPane implements DialogAware { private final ServerListPage.IconedServer reference; private final Type type; private final GridPane body = new GridPane(); - private final JFXTextField txtServerName = new JFXTextField(); - private final JFXTextField txtServerIP = new JFXTextField(); + private final JFXTextField serverNameField = new JFXTextField(); + private final JFXTextField serverIpField = new JFXTextField(); private final JFXButton btnAccept = new JFXButton(); private final JFXButton btnCancel = new JFXButton(); private final SpinnerPane spinner = new SpinnerPane(); - private final Label lblErrorMessage = new Label(); + private final Label lblErrorLabel = new Label(); + + private final SimpleBooleanProperty stillActionProgress = new SimpleBooleanProperty(false); public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference, Consumer handleCallback) { this.type = type; @@ -67,21 +71,21 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference this.handleCallback = handleCallback; getStyleClass().add("skin-pane"); + + + String referenceServerName; + String referenceServerIP; if (reference != null) { - initPaneContents(reference.getName(), reference.getIp()); + referenceServerName = reference.getName(); + referenceServerIP = reference.getIp(); } else { - initPaneContents("", ""); + referenceServerName = null; + referenceServerIP = null; } - } - - private void initPaneContents(String serverName, String serverIP) { - body.setDisable(false); - body.getChildren().clear(); - body.getColumnConstraints().clear(); - lblErrorMessage.setText(""); JFXDialogLayout rootLayout = new JFXDialogLayout(); getChildren().setAll(rootLayout); + rootLayout.setHeading(new Label(switch (type) { case EDIT -> i18n("server.manage.edit.head"); case ADD -> i18n("server.manage.add.head"); @@ -102,6 +106,7 @@ private void initPaneContents(String serverName, String serverIP) { col1.setHgrow(Priority.ALWAYS); body.getColumnConstraints().add(col1); + // server name { Label label = new Label(); @@ -109,17 +114,17 @@ private void initPaneContents(String serverName, String serverIP) { GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 0); - txtServerName.setPromptText(i18n("server.name.def")); - txtServerName.setTextFormatter(new TextFormatter<>(change -> { + serverNameField.setPromptText(i18n("server.name.def")); + serverNameField.setTextFormatter(new TextFormatter<>(change -> { if (change.getControlNewText().length() <= 32) { return change; } return null; })); - body.add(txtServerName, 1, 0); + body.add(serverNameField, 1, 0); - if (serverName != null) { - txtServerName.setText(serverName); + if (referenceServerName != null) { + serverNameField.setText(referenceServerName); } } @@ -130,12 +135,12 @@ private void initPaneContents(String serverName, String serverIP) { GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); - txtServerIP.setValidators(new RequiredValidator()); - setValidateWhileTextChanged(txtServerIP, true); - body.add(txtServerIP, 1, 1); + serverIpField.setValidators(new RequiredValidator()); + setValidateWhileTextChanged(serverIpField, true); + body.add(serverIpField, 1, 1); - if (serverIP != null) { - txtServerIP.setText(serverIP); + if (referenceServerIP != null) { + serverIpField.setText(referenceServerIP); } } @@ -147,24 +152,22 @@ private void initPaneContents(String serverName, String serverIP) { case ADD -> i18n("server.manage.add.accept"); }); btnAccept.getStyleClass().add("dialog-accept"); - btnAccept.setOnAction(e -> onAdd()); btnAccept.disableProperty().bind(new BooleanBinding() { { - bind(txtServerIP.textProperty()); + bind(serverIpField.textProperty()); } @Override protected boolean computeValue() { - return !txtServerIP.validate(); + return !serverIpField.validate(); } }); - lblErrorMessage.setWrapText(true); - lblErrorMessage.setMaxWidth(400); + lblErrorLabel.setWrapText(true); + lblErrorLabel.setMaxWidth(400); btnCancel.setText(i18n("button.cancel")); btnCancel.getStyleClass().add("dialog-cancel"); - btnCancel.setOnAction(e -> onCancel()); onEscPressed(this, btnCancel::fire); spinner.getStyleClass().add("small-spinner-pane"); @@ -173,21 +176,66 @@ protected boolean computeValue() { HBox actions = new HBox(spinner, btnCancel); actions.setAlignment(Pos.CENTER_RIGHT); - rootLayout.setActions(lblErrorMessage, actions); + rootLayout.setActions(lblErrorLabel, actions); + + btnCancel.setOnAction(e -> onCancel()); + btnAccept.setOnAction(ignored -> { + String serverIPText = serverIpField.getText(); + String serverNameText = serverNameField.getText(); + + if (!stillActionProgress.get()) { + onAdd(); + return; + } + + fireEvent(new DialogCloseEvent()); + if (reference != null) { + handleCallback.accept(reference.withIpAndName(serverIPText, serverNameText)); + } else { + handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIPText, serverNameText)); + } + }); + + ChangeListener contentChangeListener = (ignored0, ignored1, ignored2) -> { + if (stillActionProgress.get()) { + hideStillAction(); + } + }; +// serverNameField.textProperty().addListener(contentChangeListener); + serverIpField.textProperty().addListener(contentChangeListener); + } + + private void showStillAction(String errorMessage) { + stillActionProgress.set(true); + lblErrorLabel.setText(errorMessage); + btnAccept.setText(switch (type) { + case EDIT -> i18n("server.manage.edit.still"); + case ADD -> i18n("server.manage.add.still"); + }); + } + + private void hideStillAction() { + stillActionProgress.set(false); + lblErrorLabel.setText(""); + btnAccept.setText(switch (type) { + case EDIT -> i18n("server.manage.edit.accept"); + case ADD -> i18n("server.manage.add.accept"); + }); } private void onAdd() { String serverName; - String serverIP = txtServerIP.getText(); + String serverIP = serverIpField.getText(); if (serverIP == null) return; - if (txtServerName.getText() == null || txtServerName.getText().isEmpty()) { + if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { serverName = i18n("server.name.def"); } else { - serverName = txtServerName.getText(); + serverName = serverNameField.getText(); } body.setDisable(true); spinner.showSpinner(); + Task.supplyAsync(Schedulers.io(), () -> ServerStatusPinger.getStatus(serverIP) ).whenComplete(Schedulers.javafx(), (result, ignored) -> { @@ -195,23 +243,10 @@ private void onAdd() { ServerStatus status = result.getIfSucceed(); if (status == null) { spinner.hideSpinner(); - btnAccept.setText(switch (type) { - case EDIT -> i18n("server.manage.edit.still"); - case ADD -> i18n("server.manage.add.still"); - }); - btnAccept.setOnAction(e -> { - fireEvent(new DialogCloseEvent()); - if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIP, serverName)); - } else { - handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIP, serverName)); - } - }); - btnCancel.setOnAction(e -> { - initPaneContents(serverName, serverIP); - }); + body.setDisable(false); + ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); - lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { + showStillAction(switch (Objects.requireNonNull(reason)) { case EXCEPTION -> i18n("server.manage.status.error"); case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); @@ -238,6 +273,6 @@ public enum Type { @Override public void onDialogShown() { - txtServerName.requestFocus(); + serverNameField.requestFocus(); } } From 13ced3310eecffd9811c5e6644b3f99ebd7a730c Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 20:00:28 +0800 Subject: [PATCH 48/97] update EditServerPane --- .../ui/instances/server/EditServerPane.java | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index c7a750be34a..6714f2b7b95 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -180,19 +180,25 @@ protected boolean computeValue() { btnCancel.setOnAction(e -> onCancel()); btnAccept.setOnAction(ignored -> { - String serverIPText = serverIpField.getText(); - String serverNameText = serverNameField.getText(); + String serverName; + String serverIP = serverIpField.getText(); + if (serverIP == null) return; + if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { + serverName = i18n("server.name.def"); + } else { + serverName = serverNameField.getText(); + } if (!stillActionProgress.get()) { - onAdd(); + onAdd(serverName, serverIP); return; } fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIPText, serverNameText)); + handleCallback.accept(reference.withIpAndName(serverIP, serverName)); } else { - handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIPText, serverNameText)); + handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIP, serverName)); } }); @@ -223,16 +229,7 @@ private void hideStillAction() { }); } - private void onAdd() { - String serverName; - String serverIP = serverIpField.getText(); - if (serverIP == null) return; - if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { - serverName = i18n("server.name.def"); - } else { - serverName = serverNameField.getText(); - } - + private void onAdd(String serverName, String serverIP) { body.setDisable(true); spinner.showSpinner(); From 9b01512c2f8322dbacc59ec1a2dd17a5bd3a7e1d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 20:29:25 +0800 Subject: [PATCH 49/97] =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=B0=83=E6=95=B4=20Se?= =?UTF-8?q?rverListPage=20=E5=AF=B9=E6=95=B0=E6=8D=AE=E7=9A=84=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 483 +++++++++++------- .../org/jackhuang/hmcl/server/Server.java | 2 +- 2 files changed, 291 insertions(+), 194 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 0749f51dda2..b95bfc1b169 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -38,7 +38,6 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Subscription; -import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; @@ -56,6 +55,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -63,16 +63,25 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class ServerListPage extends ListPageBase { +public class ServerListPage extends ListPageBase { + private static final Object serversFileHandleLockObject = new Object(); + private final Map serverStorageMap = new LinkedHashMap<>(); + private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); private final BooleanProperty showHide = new SimpleBooleanProperty(this, "showHide", false); private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); private @Nullable HMCLGameInstance gameInstance; - private List serverHolders; + private List serverListEntries = new ArrayList<>(); private int refreshCount = 0; + public void launchAndEnterServer(ServerListItem item) { + if (gameInstance != null) { + Instances.launchAndEnterServer(gameInstance, item.server.getIp()); + } + } + public ServerListPage(ObservableValue instanceContext) { Objects.requireNonNull(instanceContext, "instanceContext"); @@ -91,116 +100,110 @@ protected Skin createDefaultSkin() { return new ServerListPageSkin(); } - public void launchAndEnterServer(ServerHolder holder) { - if (gameInstance != null) { - Instances.launchAndEnterServer(gameInstance, holder.server.getIp()); - } - } - - public void showServerStatus(ServerHolder holder) { + public void showServerStatus(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new ServerStatusPane(holder.observableServerStatus, holder.server))); + runInFX(() -> Controllers.dialog(new ServerStatusPane(item.observableServerStatus, item.server))); } } - public void editServer(ServerHolder holder) { + public void editServer(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, holder.server, (server) -> { - if (!server.equals(holder.server)) { - Task.runAsync(Schedulers.io(), () -> { - List cachedServers = holder.cachedServers; - cachedServers.set(holder.inDatPathSlot, server); - Server.saveToServersDat(cachedServers, holder.fromServersDatFilePath); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.server, (server) -> { + if (!server.equals(item.server)) { + Task.supplyAsync(Schedulers.io(), () -> { + ServerStorage storage = item.storageEntry.rootStorage(); + return storage.updateEntry(item.storageEntry, server); + }).whenComplete(Schedulers.javafx(), (newEntry, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); - refresh(); + if (newEntry == null) return; + + int index = serverListEntries.indexOf(item); + if (index != -1) { + serverListEntries.set(index, new ServerListItem(newEntry)); + } + + updateServerList(); }).start(); } }))); } } - public void copyServerIp(ServerHolder holder) { - FXUtils.copyText(holder.server.getIp(), i18n("server.manage.copy.server.ip.ok.toast")); + public void copyServerIp(ServerListItem item) { + FXUtils.copyText(item.server.getIp(), i18n("server.manage.copy.server.ip.ok.toast")); } - public void delete(ServerHolder holder) { + public void delete(ServerListItem item) { Controllers.confirm( i18n("button.remove.confirm"), i18n("server.delete"), - () -> Task.runAsync(Schedulers.io(), () -> { - List servers = holder.cachedServers; - servers.remove(holder.inDatPathSlot); - Server.saveToServersDat(servers, holder.fromServersDatFilePath); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + () -> Task.runAsync(Schedulers.io(), item.storageEntry::delete + ).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); - - refresh(); + serverListEntries.remove(item); + updateServerList(); }).start(), - null ); } - public void copyToInstance(ServerHolder holder) { - addServer(holder.server); + public void copyToInstance(ServerListItem item) { + addServer(item.server); } public void addServer(Server server) { HMCLGameInstance gameInstance = this.gameInstance; if (gameInstance == null) return; - Task.runAsync(Schedulers.io(), () -> { - Path serversDatFilePath = gameInstance.getServersDatFilePath(); - List servers; - if (Files.exists(serversDatFilePath)) { - servers = Server.loadFromServersDat(serversDatFilePath); - } else { - servers = new ArrayList<>(); + Task.supplyAsync(Schedulers.io(), () -> { + ServerStorage.ServerStorageEntry storageEntry; + synchronized (serversFileHandleLockObject) { + ServerStorage storage = serverStorageMap.get(gameInstance.getServersDatFilePath()); + if (storage == null) { + storage = new ServerStorage(gameInstance.getServersDatFilePath()); + storage.holdInstances.add(gameInstance); + } + storageEntry = storage.add(IconedServer.pack(server)); } - servers.add(server); - Server.saveToServersDat(servers, serversDatFilePath); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { + return storageEntry; + }).whenComplete(Schedulers.javafx(), (storageEntry, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); - refresh(); + serverListEntries.add(new ServerListItem(storageEntry)); + updateServerList(); }).start(); } - public void generateLaunchScript(ServerHolder holder) { + public void generateLaunchScript(ServerListItem item) { if (gameInstance != null) { - Instances.generateLaunchScriptForQuickConnectServer(gameInstance, holder.server.getIp()); + Instances.generateLaunchScriptForQuickConnectServer(gameInstance, item.server.getIp()); } } - public void loadInstance(HMCLGameInstance.Optional instance) { - this.gameInstance = instance.instance(); - refresh(); - } - private void updateServerList() { - if (serverHolders == null || gameInstance == null) { + if (serverListEntries == null || gameInstance == null) { getItems().clear(); return; } - var stream = serverHolders.stream(); + var stream = serverListEntries.stream(); if (!showAll.get()) { - stream = stream.filter(holder -> holder.fromServersDatFilePath.equals(gameInstance.getServersDatFilePath())); + stream = stream.filter(entry -> entry.storageEntry.getServerDatFilePath().equals(gameInstance.getServersDatFilePath())); } if (!showHide.get()) { - stream = stream.filter(holder -> !holder.server.isHidden()); + stream = stream.filter(entry -> !entry.server.isHidden()); } getItems().setAll(stream.toList()); } - private void addServer() { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); + public void loadInstance(HMCLGameInstance.Optional instance) { + this.gameInstance = instance.instance(); + refresh(); } private void refresh() { @@ -212,42 +215,33 @@ private void refresh() { setLoading(true); Task.supplyAsync(Schedulers.io(), () -> { - Map> pathListMap = new LinkedHashMap<>(); - for (HMCLGameInstance instance : gameInstance.getRepository().getSnapshot().getInstances()) { - if (pathListMap.containsKey(instance.getServersDatFilePath())) { - for (ServerHolder holder : pathListMap.get(instance.getServersDatFilePath())) { - holder.holdInstances.add(instance); - } - } else { - ArrayList holders = new ArrayList<>(); - pathListMap.put(instance.getServersDatFilePath(), holders); - - if (Files.exists(instance.getServersDatFilePath())) { + synchronized (serversFileHandleLockObject) { + serverStorageMap.values().forEach(ServerStorage::invalid); + serverStorageMap.clear(); + + for (HMCLGameInstance instance : gameInstance.getRepository().getSnapshot().getInstances()) { + if (serverStorageMap.containsKey(instance.getServersDatFilePath())) { + serverStorageMap.get(instance.getServersDatFilePath()).holdInstances.add(instance); + } else { try { - List parsedServers = Server.loadFromServersDat(instance.getServersDatFilePath()); - for (int index = 0; index < parsedServers.size(); index++) { - Server server = parsedServers.get(index); - ServerHolder holder = new ServerHolder(instance.getServersDatFilePath(), parsedServers, index, IconedServer.pack(server)); - holder.holdInstances.add(instance); - - holders.add(holder); - } + ServerStorage storage = new ServerStorage(instance.getServersDatFilePath()); + storage.holdInstances.add(instance); + serverStorageMap.put(instance.getServersDatFilePath(), storage); } catch (IOException e) { LOG.error("Failed to load servers from dat file: " + instance.getServersDatFilePath(), e); } } } - } - - return pathListMap.values().stream().flatMap(Collection::stream).toList(); + return serverStorageMap.values().stream().flatMap(e -> e.storageEntries.stream()).map(ServerListItem::new).toList(); + } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (refreshCount != currentRefresh) { // A newer refresh task is running, discard this result return; } - serverHolders = result; + serverListEntries = new ArrayList<>(result); updateServerList(); if (exception != null) @@ -257,7 +251,162 @@ private void refresh() { }).start(); } - private static final class ServerListCell extends ListCell { + private void addServer() { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); + } + + private static final class ServerStorage { + private final List holdInstances = new ArrayList<>(); + private final Path serverDatFilePath; + private final List storageEntries; + private boolean storageValid = true; + + private ServerStorage(Path serverDatFilePath) throws IOException { + this.serverDatFilePath = serverDatFilePath; + + synchronized (serversFileHandleLockObject) { + if (Files.exists(serverDatFilePath)) { + List servers = Server.loadFromServersDat(serverDatFilePath); + storageEntries = new CopyOnWriteArrayList<>(); + for (Server server : servers) { + ServerStorageEntry entry = new ServerStorageEntry(IconedServer.pack(server)); + storageEntries.add(entry); + } + } else { + storageEntries = new CopyOnWriteArrayList<>(); + } + } + } + + private void save() throws IOException { + synchronized (serversFileHandleLockObject) { + if (!storageValid) return; + + Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); + } + } + + private void invalid() { + storageValid = false; + } + + private void deleteEntry(ServerStorageEntry entry) throws IOException { + synchronized (serversFileHandleLockObject) { + if (!storageValid) return; + if (storageEntries.remove(entry)) { + save(); + } + } + } + + private @Nullable ServerStorageEntry updateEntry(ServerStorageEntry entry, IconedServer newServer) throws IOException { + synchronized (serversFileHandleLockObject) { + if (!storageValid) return null; + + int index = storageEntries.indexOf(entry); + if (index == -1) return null; + + ServerStorageEntry newEntry = new ServerStorageEntry(newServer); + storageEntries.set(index, newEntry); + + save(); + + return newEntry; + } + } + + public @Nullable ServerStorageEntry add(IconedServer server) throws IOException { + synchronized (serversFileHandleLockObject) { + if (!storageValid) return null; + ServerStorageEntry entry = new ServerStorageEntry(IconedServer.pack(server)); + storageEntries.add(entry); + save(); + return entry; + } + } + + public class ServerStorageEntry { + private final IconedServer server; + + public ServerStorageEntry(IconedServer server) { + this.server = server; + } + + private void delete() throws IOException { + ServerStorage.this.deleteEntry(this); + } + + private ServerStorage rootStorage() { + return ServerStorage.this; + } + + private Path getServerDatFilePath() { + return serverDatFilePath; + } + } + } + + public static final class ServerListItem { + final @NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry; + final @NotNull IconedServer server; + + final @NotNull ObservableServerStatus observableServerStatus; + + public ServerListItem(@NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry) { + this(storageEntry, new ObservableServerStatus(storageEntry.server.getIp())); + } + + public ServerListItem(@NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { + this.storageEntry = storageEntry; + this.server = storageEntry.server; + this.observableServerStatus = observableServerStatus; + } + } + + public static class IconedServer extends Server { + final Image iconImage; + + public IconedServer(ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { + super(serverPackStatus, hidden, icon, ip, name); + + iconImage = parseImageOrDefault(icon); + } + + public static IconedServer pack(Server server) { + if (server instanceof IconedServer) { + return (IconedServer) server; + } + return new IconedServer( + server.getServerPackStatus(), + server.isHidden(), + server.getIcon(), + server.getIp(), + server.getName() + ); + } + + public static @NotNull Image parseImageOrDefault(@Nullable String imageBase64String) { + if (imageBase64String != null && !imageBase64String.isEmpty()) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { + // png format. + return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); + } catch (Exception e) { + LOG.warning("Failed to decode server icon", e); + } + } + return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); + } + + public IconedServer withIcon(@Nullable String newIcon) { + return new IconedServer(getServerPackStatus(), isHidden(), newIcon, getIp(), getName()); + } + + public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { + return new IconedServer(getServerPackStatus(), isHidden(), getIcon(), newIp, newName); + } + } + + private final class ServerListCell extends ListCell { private final ServerListPage page; @@ -310,9 +459,9 @@ public ServerListCell(ServerListPage page) { statusBtn = FXUtils.newToggleButton4(SVG.NONE); statusBtn.managedProperty().bind(statusBtn.visibleProperty()); statusBtn.setOnAction(event -> { - ServerHolder holder = getItem(); - if (holder != null) - page.showServerStatus(holder); + ServerListItem item = getItem(); + if (item != null) + page.showServerStatus(item); }); serverNetworkLatencyPane = new ServerNetworkLatencyPane(); @@ -327,26 +476,26 @@ public ServerListCell(ServerListPage page) { right.getChildren().add(editBtn); FXUtils.installFastTooltip(editBtn, i18n("server.manage.edit")); editBtn.setOnAction(event -> { - ServerHolder holder = getItem(); - if (holder != null) - page.editServer(holder); + ServerListItem item = getItem(); + if (item != null) + page.editServer(item); }); JFXButton launchBtn = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); right.getChildren().add(launchBtn); FXUtils.installFastTooltip(launchBtn, i18n("instance.launch")); launchBtn.setOnAction(event -> { - ServerHolder holder = getItem(); - if (holder != null) - page.launchAndEnterServer(holder); + ServerListItem item = getItem(); + if (item != null) + page.launchAndEnterServer(item); }); JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); right.getChildren().add(btnMore); btnMore.setOnAction(event -> { - ServerHolder holder = getItem(); - if (holder != null) - showPopupMenu(holder, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); + ServerListItem item = getItem(); + if (item != null) + showPopupMenu(item, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); }); } @@ -355,22 +504,22 @@ public ServerListCell(ServerListPage page) { if (event.getClickCount() != 1) return; - ServerHolder holder = getItem(); - if (holder == null) + ServerListItem item = getItem(); + if (item == null) return; if (event.getButton() == MouseButton.SECONDARY) - showPopupMenu(holder, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); + showPopupMenu(item, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); }); } @Override - protected void updateItem(ServerHolder holder, boolean empty) { - ServerHolder oldHolder = getItem(); + protected void updateItem(ServerListItem item, boolean empty) { + ServerListItem oldItem = getItem(); boolean oldEmpty = isEmpty(); - super.updateItem(holder, empty); - if (oldHolder == holder && oldEmpty == empty) return; + super.updateItem(item, empty); + if (oldItem == item && oldEmpty == empty) return; if (serverStatusPingingValueSubscription != null) { serverStatusPingingValueSubscription.unsubscribe(); @@ -384,55 +533,55 @@ protected void updateItem(ServerHolder holder, boolean empty) { this.graphic.releaseRippleImmediately(); this.contentLine1.getTags().clear(); - if (empty || holder == null) { + if (empty || item == null) { setGraphic(null); serverIcon.setImage(null); contentLine1.setTitle(""); - serverNetworkLatencyPane.error(); +// serverNetworkLatencyPane.error(); statusBtnTooltip.setText(""); contentLine2AddressMaskPane.set(""); } else { - serverIcon.setImage(holder.server.iconImage); - contentLine1.setTitle(holder.server.getName() != null ? parseColorEscapes(holder.server.getName()) : ""); + serverIcon.setImage(item.server.iconImage); + contentLine1.setTitle(item.server.getName() != null ? parseColorEscapes(item.server.getName()) : ""); - contentLine2AddressMaskPane.set(holder.server.getIp()); + contentLine2AddressMaskPane.set(item.server.getIp()); - if (holder.server.isHidden()) { + if (item.server.isHidden()) { contentLine1.addTag(i18n("server.tag.hide")); } - if (holder.holdInstances.contains(page.gameInstance)) { + if (item.storageEntry.rootStorage().holdInstances.contains(page.gameInstance)) { contentLine1.addTag(i18n("server.tag.hold.current")); - holder.holdInstances.stream() + item.storageEntry.rootStorage().holdInstances.stream() .filter(e -> !e.equals(page.gameInstance)) .map(gameInstance -> gameInstance.getId().id()) .forEach(contentLine1::addTag); } else { - holder.holdInstances.stream() + item.storageEntry.rootStorage().holdInstances.stream() .map(gameInstance -> gameInstance.getId().id()) .forEach(contentLine1::addTag); } setGraphic(graphic); - holder.observableServerStatus.refreshIfNoResultAsync(false); + item.observableServerStatus.refreshIfNoResultAsync(false); - applyServerStatusResult(holder, holder.observableServerStatus.resultProperty().get()); - serverStatusResultValueSubscription = holder.observableServerStatus.resultProperty().subscribe(result -> applyServerStatusResult(holder, result)); + applyServerStatusResult(item, item.observableServerStatus.resultProperty().get()); + serverStatusResultValueSubscription = item.observableServerStatus.resultProperty().subscribe(result -> applyServerStatusResult(item, result)); - applyServerPinging(holder, holder.observableServerStatus.pingingProperty().get()); - serverStatusPingingValueSubscription = holder.observableServerStatus.pingingProperty().subscribe(pinging -> applyServerPinging(holder, pinging)); + applyServerPinging(item, item.observableServerStatus.pingingProperty().get()); + serverStatusPingingValueSubscription = item.observableServerStatus.pingingProperty().subscribe(pinging -> applyServerPinging(item, pinging)); } } - private void applyServerPinging(ServerHolder holder, boolean pinging) { + private void applyServerPinging(ServerListItem item, boolean pinging) { if (pinging) { serverNetworkLatencyPane.ping(); statusBtnTooltip.setText(i18n("server.manage.status.outside.pinging")); } } - private void applyServerStatusResult(ServerHolder holder, ServerStatusResult result) { + private void applyServerStatusResult(ServerListItem item, ServerStatusResult result) { if (result == null) { // pinging..., skip return; @@ -451,24 +600,33 @@ private void applyServerStatusResult(ServerHolder holder, ServerStatusResult res statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); // update latest server icon serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); -// if (!Objects.equals(serverStatus.favicon(), holder.server.getIcon())) { -// // save latest icon -// Task.runAsync(Schedulers.io(), () -> { -// try { -// IconedServer newServer = holder.server.withIcon(serverStatus.favicon()); -// holder.cachedServers.set(holder.inDatPathSlot, newServer); -// Server.saveToServersDat(holder.cachedServers, holder.fromServersDatFilePath); -// } catch (Exception e) { -// LOG.error("Failed to save servers dat", e); -// } -// }).start(); -// } + + if (!Objects.equals(serverStatus.favicon(), item.server.getIcon())) { + // save latest icon + Task.supplyAsync(Schedulers.io(), () -> { + IconedServer newServer = item.server.withIcon(serverStatus.favicon()); + ServerStorage storage = item.storageEntry.rootStorage(); + return storage.updateEntry(item.storageEntry, newServer); + }).whenComplete(Schedulers.javafx(), (newEntry, exception) -> { + if (exception != null) + LOG.warning("Failed to save server data.", exception); + + if (newEntry == null) return; + + int index = serverListEntries.indexOf(item); + if (index != -1) { + serverListEntries.set(index, new ServerListItem(newEntry, item.observableServerStatus)); + } + + updateServerList(); + }).start(); + } } } // Popup Menu - public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { + public void showPopupMenu(ServerListPage.ServerListItem holder, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); @@ -498,7 +656,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos ) ); if (page.gameInstance != null) { - copyToInstanceMEnuItem.setDisable(holder.fromServersDatFilePath.equals(page.gameInstance.getServersDatFilePath())); + copyToInstanceMEnuItem.setDisable(holder.storageEntry.getServerDatFilePath().equals(page.gameInstance.getServersDatFilePath())); } JFXPopup.PopupVPosition vPosition = determineOptimalPopupPosition(this, popup); @@ -506,68 +664,7 @@ public void showPopupMenu(ServerListPage.ServerHolder holder, JFXPopup.PopupHPos } } - public static class IconedServer extends Server { - final Image iconImage; - - public IconedServer(ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { - super(serverPackStatus, hidden, icon, ip, name); - - iconImage = parseImageOrDefault(icon); - } - - public static IconedServer pack(Server server) { - if (server instanceof IconedServer) { - return (IconedServer) server; - } - return new IconedServer( - server.getServerPackStatus(), - server.isHidden(), - server.getIcon(), - server.getIp(), - server.getName() - ); - } - - public IconedServer withIcon(@Nullable String newIcon) { - return new IconedServer(getServerPackStatus(), isHidden(), newIcon, getIp(), getName()); - } - - public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { - return new IconedServer(getServerPackStatus(), isHidden(), getIcon(), newIp, newName); - } - - public static @NotNull Image parseImageOrDefault(@Nullable String imageBase64String) { - if (imageBase64String != null && !imageBase64String.isEmpty()) { - try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { - // png format. - return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); - } catch (Exception e) { - LOG.warning("Failed to decode server icon", e); - } - } - return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); - } - } - - public static final class ServerHolder { - final @NotNull List holdInstances = new ArrayList<>(); - final @NotNull Path fromServersDatFilePath; - final @NotNull List cachedServers; - final int inDatPathSlot; - final @NotNull IconedServer server; - - final @NotNull ObservableServerStatus observableServerStatus; - - public ServerHolder(@NotNull Path fromServersDatFilePath, @NotNull List cachedServers, int inDatPathSlot, @NotNull IconedServer server) { - this.fromServersDatFilePath = fromServersDatFilePath; - this.inDatPathSlot = inDatPathSlot; - this.cachedServers = cachedServers; - this.server = server; - this.observableServerStatus = new ObservableServerStatus(server.getIp()); - } - } - - private final class ServerListPageSkin extends ToolbarListPageSkin { + private final class ServerListPageSkin extends ToolbarListPageSkin { ServerListPageSkin() { super(ServerListPage.this); @@ -596,7 +693,7 @@ protected List initializeToolbar(ServerListPage skinnable) { } @Override - protected ListCell createListCell(JFXListView listView) { + protected ListCell createListCell(JFXListView listView) { return new ServerListCell(getSkinnable()); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 9d47c44d763..23a7666967e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -76,7 +76,7 @@ else if (!(serversTag instanceof ListTag)) return servers; } - public static void saveToServersDat(@NotNull List servers, @NotNull Path file) throws IOException { + public static void saveToServersDat(@NotNull List servers, @NotNull Path file) throws IOException { ListTag tag = new ListTag<>(); for (Server server : servers) { CompoundTag serverTag = new CompoundTag(); From ed0d6818297d2a3c2a6639c8133d9ed1be8fdc43 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 21:40:36 +0800 Subject: [PATCH 50/97] =?UTF-8?q?=E7=BB=86=E8=8A=82=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 90 +++++++----- .../{HostAndIp.java => HostAndPort.java} | 2 +- .../hmcl/server/resolver/ServerAddress.java | 16 +-- .../resolver/ServerDnsSrvRedirector.java | 6 +- .../util/MinecraftChatComponentUtils.java | 130 +++++++++++------- 5 files changed, 148 insertions(+), 96 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/{HostAndIp.java => HostAndPort.java} (97%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index b95bfc1b169..91520d575c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -56,6 +56,7 @@ import java.nio.file.Path; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.UnaryOperator; import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; @@ -112,19 +113,20 @@ public void editServer(ServerListItem item) { if (!server.equals(item.server)) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, server); - }).whenComplete(Schedulers.javafx(), (newEntry, exception) -> { + return storage.updateEntry(item.storageEntry, oldValue -> oldValue.withIpAndName(server.getIp(), server.getName())); + }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); - if (newEntry == null) return; - - int index = serverListEntries.indexOf(item); - if (index != -1) { - serverListEntries.set(index, new ServerListItem(newEntry)); + if (updated) { + int index = serverListEntries.indexOf(item); + if (index != -1) { + serverListEntries.set(index, new ServerListItem(item.storageEntry)); + } + updateServerList(); } - updateServerList(); + }).start(); } }))); @@ -141,10 +143,12 @@ public void delete(ServerListItem item) { i18n("server.delete"), () -> Task.runAsync(Schedulers.io(), item.storageEntry::delete ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) + if (exception != null) { LOG.warning("Failed to save server data.", exception); - serverListEntries.remove(item); - updateServerList(); + } else { + serverListEntries.remove(item); + updateServerList(); + } }).start(), null ); @@ -165,6 +169,7 @@ public void addServer(Server server) { if (storage == null) { storage = new ServerStorage(gameInstance.getServersDatFilePath()); storage.holdInstances.add(gameInstance); + serverStorageMap.put(gameInstance.getServersDatFilePath(), storage); } storageEntry = storage.add(IconedServer.pack(server)); } @@ -293,25 +298,41 @@ private void invalid() { private void deleteEntry(ServerStorageEntry entry) throws IOException { synchronized (serversFileHandleLockObject) { if (!storageValid) return; - if (storageEntries.remove(entry)) { - save(); + + int index = storageEntries.indexOf(entry); + if (index != -1) { + ServerStorageEntry removed = storageEntries.remove(index); + try { + save(); + } catch (IOException e) { + storageEntries.add(index, removed); + throw e; + } } } } - private @Nullable ServerStorageEntry updateEntry(ServerStorageEntry entry, IconedServer newServer) throws IOException { + private boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull IconedServer> updater) throws IOException { synchronized (serversFileHandleLockObject) { - if (!storageValid) return null; + if (!storageValid) return false; int index = storageEntries.indexOf(entry); - if (index == -1) return null; + if (index == -1) return false; - ServerStorageEntry newEntry = new ServerStorageEntry(newServer); - storageEntries.set(index, newEntry); + IconedServer oldValue = entry.server; + IconedServer newValue = updater.apply(oldValue); - save(); + if (oldValue.equals(newValue)) return false; - return newEntry; + try { + entry.server = newValue; + save(); + } catch (IOException e) { + entry.server = oldValue; + throw e; + } + + return true; } } @@ -319,14 +340,21 @@ private void deleteEntry(ServerStorageEntry entry) throws IOException { synchronized (serversFileHandleLockObject) { if (!storageValid) return null; ServerStorageEntry entry = new ServerStorageEntry(IconedServer.pack(server)); - storageEntries.add(entry); - save(); + + try { + storageEntries.add(entry); + save(); + } catch (IOException e) { + storageEntries.remove(entry); + throw e; + } + return entry; } } public class ServerStorageEntry { - private final IconedServer server; + private volatile IconedServer server; public ServerStorageEntry(IconedServer server) { this.server = server; @@ -604,21 +632,21 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res if (!Objects.equals(serverStatus.favicon(), item.server.getIcon())) { // save latest icon Task.supplyAsync(Schedulers.io(), () -> { - IconedServer newServer = item.server.withIcon(serverStatus.favicon()); ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, newServer); - }).whenComplete(Schedulers.javafx(), (newEntry, exception) -> { + return storage.updateEntry(item.storageEntry, server -> server.withIcon(serverStatus.favicon())); + }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); - if (newEntry == null) return; + if (updated) { + int index = serverListEntries.indexOf(item); + if (index != -1) { + serverListEntries.set(index, new ServerListItem(item.storageEntry, item.observableServerStatus)); - int index = serverListEntries.indexOf(item); - if (index != -1) { - serverListEntries.set(index, new ServerListItem(newEntry, item.observableServerStatus)); + updateServerList(); + } } - updateServerList(); }).start(); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java similarity index 97% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java index 5ba41aacd1e..7c101504980 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndIp.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java @@ -23,7 +23,7 @@ import java.net.InetSocketAddress; import java.net.UnknownHostException; -public record HostAndIp( +public record HostAndPort( @NotNull String host, int port ) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java index 44964c192a8..50bf0181795 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java @@ -26,14 +26,14 @@ import java.util.Map; public final class ServerAddress { - private final HostAndIp hostAndIp; + private final HostAndPort hostAndPort; private final Map queryProperties; private ServerAddress( - HostAndIp hostAndIp, + HostAndPort hostAndPort, Map queryProperties ) { - this.hostAndIp = hostAndIp; + this.hostAndPort = hostAndPort; this.queryProperties = queryProperties; } @@ -85,16 +85,16 @@ public static ServerAddress fromString(String input) throws IOException { } catch (Exception ignore) { } } - return new ServerAddress(new HostAndIp(host, port), queryProperties); + return new ServerAddress(new HostAndPort(host, port), queryProperties); } public ServerAddressResolveResult resolve() { try { - InetSocketAddress inetSocketAddress = hostAndIp.toInetSocketAddress(); + InetSocketAddress inetSocketAddress = hostAndPort.toInetSocketAddress(); if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG); } - HostAndIp lookupDnsResult = ServerDnsSrvRedirector.lookup(this); + HostAndPort lookupDnsResult = ServerDnsSrvRedirector.lookup(this); if (lookupDnsResult != null) { inetSocketAddress = lookupDnsResult.toInetSocketAddress(); if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { @@ -111,7 +111,7 @@ public Map getQueryProperties() { return queryProperties; } - public HostAndIp getHostAndIp() { - return hostAndIp; + public HostAndPort getHostAndIp() { + return hostAndPort; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java index e23636a5376..b5db5a82c9f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java @@ -35,7 +35,7 @@ private ServerDnsSrvRedirector() { } - static @Nullable HostAndIp lookup(ServerAddress address0) { + static @Nullable HostAndPort lookup(ServerAddress address0) { if (redirector == null) { synchronized (ServerDnsSrvRedirector.class) { if (redirector == null) { @@ -62,7 +62,7 @@ private ServerDnsSrvRedirector() { } catch (Exception ignored) { } - return new HostAndIp(arguments[3], port); + return new HostAndPort(arguments[3], port); } } catch (Throwable ignored) { } @@ -81,6 +81,6 @@ private ServerDnsSrvRedirector() { @FunctionalInterface private interface DnsSrvRedirector { - @Nullable HostAndIp lookup(@NotNull ServerAddress address); + @Nullable HostAndPort lookup(@NotNull ServerAddress address); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java index 9d1e34110db..565c40354da 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java @@ -22,9 +22,13 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; +import java.util.regex.Pattern; + public final class MinecraftChatComponentUtils { - private MinecraftChatComponentUtils() { + private static final Pattern FORMATTING_CODE_PATTERN = Pattern.compile("§.?"); + + private MinecraftChatComponentUtils() { } public static String toPlainStringFromChatComponent(JsonElement chat) { @@ -34,7 +38,38 @@ public static String toPlainStringFromChatComponent(JsonElement chat) { StringBuilder sb = new StringBuilder(); extractText(chat, sb); - return sb.toString().replaceAll("§.?", ""); + return FORMATTING_CODE_PATTERN.matcher(sb).replaceAll(""); + } + + private static void extractText(JsonPrimitive prim, StringBuilder sb) { + if (prim == null || prim.isJsonNull()) { + return; + } + sb.append(prim.getAsString()); + } + + private static void extractText(JsonArray array, StringBuilder sb) { + if (array == null) { + return; + } + for (JsonElement child : array) { + extractText(child, sb); + } + } + + private static boolean hasJsonArray(JsonObject obj, String key) { + JsonElement element = obj.get(key); + return element instanceof JsonArray; + } + + private static boolean hasJsonObject(JsonObject obj, String key) { + JsonElement element = obj.get(key); + return element instanceof JsonObject; + } + + private static boolean hasJsonPrimitive(JsonObject obj, String key) { + JsonElement element = obj.get(key); + return element instanceof JsonPrimitive; } private static void extractText(JsonElement chat, StringBuilder sb) { @@ -43,75 +78,64 @@ private static void extractText(JsonElement chat, StringBuilder sb) { } if (chat.isJsonPrimitive()) { - JsonPrimitive prim = chat.getAsJsonPrimitive(); - if (prim.isString()) { - sb.append(prim.getAsString()); - } else if (prim.isBoolean()) { - sb.append(prim.getAsBoolean()); - } else { - sb.append(prim.getAsNumber().toString()); - } + extractText(chat.getAsJsonPrimitive(), sb); return; } if (chat.isJsonArray()) { - for (JsonElement child : chat.getAsJsonArray()) { - extractText(child, sb); - } + extractText(chat.getAsJsonArray(), sb); return; } if (chat.isJsonObject()) { JsonObject obj = chat.getAsJsonObject(); - if (obj.has("text")) { - JsonElement textEl = obj.get("text"); - if (textEl.isJsonPrimitive()) { - sb.append(textEl.getAsString()); - } - } - - if (obj.has("translate")) { - sb.append(obj.get("translate").getAsString()); - if (obj.has("with")) { - JsonArray withArr = obj.getAsJsonArray("with"); - for (JsonElement w : withArr) { - extractText(w, sb); - } - } + if (hasJsonPrimitive(obj, "text")) { + extractText(obj.getAsJsonPrimitive("text"), sb); + } else if (hasJsonPrimitive(obj, "translate")) { + extractTranslate(obj, sb); + } else if (hasJsonObject(obj, "score")) { + extractScore(obj.getAsJsonObject("score"), sb); + } else if (hasJsonPrimitive(obj, "selector")) { + extractText(obj.getAsJsonPrimitive("selector"), sb); + } else if (hasJsonPrimitive(obj, "keybind")) { + extractText(obj.getAsJsonPrimitive("keybind"), sb); + } else if (hasJsonPrimitive(obj, "nbt")) { + extractText(obj.getAsJsonPrimitive("nbt"), sb); } - if (obj.has("score")) { - JsonObject scoreObj = obj.getAsJsonObject("score"); - if (scoreObj.has("name")) sb.append(scoreObj.get("name").getAsString()); - if (scoreObj.has("objective")) sb.append(":").append(scoreObj.get("objective").getAsString()); + if (hasJsonArray(obj, "extra")) { + extractText(obj.getAsJsonArray("extra"), sb); } - if (obj.has("selector")) { - sb.append(obj.get("selector").getAsString()); - } - - if (obj.has("keybind")) { - sb.append(obj.get("keybind").getAsString()); + if (hasJsonArray(obj, "rawtext")) { + extractText(obj.getAsJsonArray("rawtext"), sb); } + } + } - if (obj.has("nbt")) { - sb.append(obj.get("nbt").getAsString()); - } + private static void extractTranslate(JsonObject obj, StringBuilder sb) { + if (hasJsonPrimitive(obj, "fallback")) { + extractText(obj.getAsJsonPrimitive("fallback"), sb); + } else { + extractText(obj.getAsJsonPrimitive("translate"), sb); + } - if (obj.has("rawtext")) { - JsonArray rawArr = obj.getAsJsonArray("rawtext"); - for (JsonElement child : rawArr) { - extractText(child, sb); - } - } + if (hasJsonArray(obj, "with")) { + extractText(obj.getAsJsonArray("with"), sb); + } + } - if (obj.has("extra")) { - JsonArray extraArr = obj.getAsJsonArray("extra"); - for (JsonElement child : extraArr) { - extractText(child, sb); - } - } + private static void extractScore(JsonObject scoreObj, StringBuilder sb) { + if (hasJsonPrimitive(scoreObj, "value")) { + extractText(scoreObj.getAsJsonPrimitive("value"), sb); + return; + } + if (hasJsonPrimitive(scoreObj, "name")) { + extractText(scoreObj.getAsJsonPrimitive("name"), sb); + } + if (hasJsonPrimitive(scoreObj, "objective")) { + sb.append(':').append(scoreObj.get("objective").getAsString()); } } } From 2c2b684c6faa65849ebe00ddb8a5989dd3ec7918 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 22:19:14 +0800 Subject: [PATCH 51/97] =?UTF-8?q?=E7=BB=86=E8=8A=82=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerListPage.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 91520d575c4..f685a2c14c1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -175,11 +175,14 @@ public void addServer(Server server) { } return storageEntry; }).whenComplete(Schedulers.javafx(), (storageEntry, exception) -> { - if (exception != null) + if (exception != null) { LOG.warning("Failed to save server data.", exception); + } - serverListEntries.add(new ServerListItem(storageEntry)); - updateServerList(); + if (storageEntry != null) { + serverListEntries.add(new ServerListItem(storageEntry)); + updateServerList(); + } }).start(); } From 919537904d408c62d1f6568fe5e33f6b779b467e Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Wed, 7 Oct 2026 22:24:20 +0800 Subject: [PATCH 52/97] =?UTF-8?q?=E5=BD=93=E5=BD=93=E5=89=8D=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E4=B8=BA=20ServerListPage=20=E6=97=B6=EF=BC=8C?= =?UTF-8?q?=E6=8B=96=E6=8B=BD=E6=B7=BB=E5=8A=A0=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E6=97=B6=E4=B8=8D=E5=86=8D=E9=87=8D=E6=96=B0=E6=89=93=E5=BC=80?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerStatusPane.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 8bd0ae5de31..f7ddd72c199 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -98,9 +98,13 @@ public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedS gotoDownload, null); } else { - Instances.modifyServerList(selectedInstance); if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { listPage.addServer(iconedServer); + } else { + Instances.modifyServerList(selectedInstance); + if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { + listPage.addServer(iconedServer); + } } } }); From 3073d34020dc432ac6fb546b66330c2ca6564e74 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 8 Oct 2026 20:03:13 +0800 Subject: [PATCH 53/97] =?UTF-8?q?ping=20error=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 11 +- .../server/ServerNetworkLatencyPane.java | 23 +++- HMCL/src/main/resources/assets/css/root.css | 121 +++++++++--------- .../hmcl/server/ServerStatusPinger.java | 2 +- 4 files changed, 85 insertions(+), 72 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index f685a2c14c1..ea401ae212b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -248,14 +248,15 @@ private void refresh() { // A newer refresh task is running, discard this result return; } + setLoading(false); - serverListEntries = new ArrayList<>(result); - updateServerList(); - - if (exception != null) + if (exception != null) { LOG.warning("Failed to load server list page", exception); + } else { + serverListEntries = new ArrayList<>(result); + updateServerList(); + } - setLoading(false); }).start(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java index 1497b9d3301..3e972a6125c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java @@ -22,6 +22,7 @@ import javafx.animation.Timeline; import javafx.scene.Node; import javafx.scene.layout.StackPane; +import javafx.scene.shape.SVGPath; import javafx.util.Duration; import org.jackhuang.hmcl.ui.SVG; @@ -79,22 +80,32 @@ private void next() { public void error() { state = State.ERROR; getChildren().clear(); - getChildren().add(SVG.SERVER_SIGNAL_ERROR.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_ERROR, "-fx-fill: -monet-error;")); + } + + private SVGPath createIcon(SVG svg, String style) { + SVGPath icon = createIcon(svg); + icon.setStyle(style); + return icon; + } + + private SVGPath createIcon(SVG svg) { + return svg.createIcon(); } public void pong(long networkLatency) { state = State.PONG; getChildren().clear(); if (networkLatency < 150) { - getChildren().add(SVG.SERVER_SIGNAL_FULL.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_FULL)); } else if (networkLatency < 300) { - getChildren().add(SVG.SERVER_SIGNAL_3_BAR.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_3_BAR)); } else if (networkLatency < 600) { - getChildren().add(SVG.SERVER_SIGNAL_2_BAR.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_2_BAR)); } else if (networkLatency < 1000) { - getChildren().add(SVG.SERVER_SIGNAL_1_BAR.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_1_BAR)); } else { - getChildren().add(SVG.SERVER_SIGNAL_0_BAR.createIcon()); + getChildren().add(createIcon(SVG.SERVER_SIGNAL_0_BAR)); } } diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index eb2cd815675..3e7966baec7 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -582,66 +582,6 @@ -fx-padding: 0.7em 0.8em; } -/******************************************************************************* - * * - * Server Status Dialog * - * * - ******************************************************************************/ - -.server-status-motd-box { - -fx-background-color: -monet-surface-container; - -fx-background-radius: 10px; - -fx-border-color: -monet-outline-variant; - -fx-border-width: 1px; - -fx-border-radius: 10px; - -fx-max-width: 620px; -} - -.server-status-motd-title { - -fx-font-size: 12px; - -fx-font-weight: 700; - -fx-text-fill: -monet-on-surface; -} - -.server-status-motd-line { - -fx-font-size: 13px; - -fx-text-fill: -monet-on-surface; - -fx-wrap-text: true; - -fx-max-width: 560px; -} - -.server-status-detail-row { - -fx-background-color: -monet-surface-container-low; - -fx-background-radius: 10px; - -fx-border-color: -monet-outline-variant; - -fx-border-width: 1px; - -fx-border-radius: 10px; - -fx-max-width: 620px; -} - -.server-status-stat-cell { - -fx-border-color: -monet-outline-variant; - -fx-border-width: 0 1 0 0; -} - -.server-status-stat-cell-last { - -fx-border-width: 0; -} - -.server-status-stat-title { - -fx-font-size: 12px; - -fx-font-weight: 600; - -fx-text-fill: -monet-on-surface-variant; -} - -.server-status-stat-value { - -fx-font-size: 13px; - -fx-font-weight: 500; - -fx-text-fill: -monet-on-surface; - -fx-wrap-text: true; - -fx-max-width: 100px; -} - /******************************************************************************* * * * Microsoft Login Dialog * @@ -2074,3 +2014,64 @@ .addon-changelog-table-cell { -fx-text-fill: -monet-primary; } + + +/******************************************************************************* + * * + * Server Status * + * * + ******************************************************************************/ + +.server-status-motd-box { + -fx-background-color: -monet-surface-container; + -fx-background-radius: 10px; + -fx-border-color: -monet-outline-variant; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-max-width: 620px; +} + +.server-status-motd-title { + -fx-font-size: 12px; + -fx-font-weight: 700; + -fx-text-fill: -monet-on-surface; +} + +.server-status-motd-line { + -fx-font-size: 13px; + -fx-text-fill: -monet-on-surface; + -fx-wrap-text: true; + -fx-max-width: 560px; +} + +.server-status-detail-row { + -fx-background-color: -monet-surface-container-low; + -fx-background-radius: 10px; + -fx-border-color: -monet-outline-variant; + -fx-border-width: 1px; + -fx-border-radius: 10px; + -fx-max-width: 620px; +} + +.server-status-stat-cell { + -fx-border-color: -monet-outline-variant; + -fx-border-width: 0 1 0 0; +} + +.server-status-stat-cell-last { + -fx-border-width: 0; +} + +.server-status-stat-title { + -fx-font-size: 12px; + -fx-font-weight: 600; + -fx-text-fill: -monet-on-surface-variant; +} + +.server-status-stat-value { + -fx-font-size: 13px; + -fx-font-weight: 500; + -fx-text-fill: -monet-on-surface; + -fx-wrap-text: true; + -fx-max-width: 100px; +} \ No newline at end of file diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index 37a21e7229f..de6f4aea3fd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -158,7 +158,7 @@ private static long readPongResponsePacket(DataInputStream in) throws IOExceptio private static T readPacket(DataInputStream in, PacketReader reader) throws IOException { int length = readVarInt(in); - if (length > 32768) throw new IOException("Packet length too large: " + length); + if (length > 2097151) throw new IOException("Packet length too large: " + length); byte[] packetData = new byte[length]; in.readFully(packetData); From 8892f2079429faaabeac0958c52cf56a05621fdb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 8 Oct 2026 22:43:35 +0800 Subject: [PATCH 54/97] update server pinger --- .../ServerBlockedByMojangException.java | 26 ++++++ .../hmcl/server/ServerStatusPinger.java | 80 +++++++++++-------- .../hmcl/server/ServerStatusResult.java | 18 ++++- .../hmcl/server/resolver/ServerAddress.java | 20 +++-- .../resolver/ServerAddressResolveResult.java | 48 ++++++----- .../resolver/ServerDnsSrvRedirector.java | 4 +- 6 files changed, 129 insertions(+), 67 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerBlockedByMojangException.java diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerBlockedByMojangException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerBlockedByMojangException.java new file mode 100644 index 00000000000..14564ecd1e6 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerBlockedByMojangException.java @@ -0,0 +1,26 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import java.io.IOException; + +public class ServerBlockedByMojangException extends IOException { + public ServerBlockedByMojangException(String message) { + super(message); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java index de6f4aea3fd..8051b6ad923 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java @@ -33,48 +33,60 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping +// https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#1.6 // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Minecraft_Forge_Handshake public final class ServerStatusPinger { private ServerStatusPinger() { } - public static @NotNull ServerStatusResult getStatus(String serverIp) throws IOException { - try { - ServerAddress address = ServerAddress.fromString(serverIp); - ServerAddressResolveResult resolveResult = address.resolve(); - if (resolveResult instanceof ServerAddressResolveResult.SuccessResult successResult) { - try (Socket socket = new Socket()) { - socket.setOption(StandardSocketOptions.TCP_NODELAY, true); - socket.connect(successResult.getConnectAddress(), 7_000); - socket.setSoTimeout(7_000); - - try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); - DataInputStream in = new DataInputStream(socket.getInputStream())) { - - sendHandshakeStatusPacket(out, - successResult.getPackerHandshakeAddress(), successResult.getPackerHandshakePort() - ); - sendStatusRequestPacket(out); - var status = readStatusResponsePacket(in); - - long sendTime = System.currentTimeMillis(); - sendPingRequestPacket(out); - var pong = readPongResponsePacket(in); - long networkLatency = System.currentTimeMillis() - sendTime; - - JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); - - ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); - return ServerStatusResult.success(parseServerStatus); - } - } - } else { - return ServerStatusResult.failure(((ServerAddressResolveResult.FailureResult) resolveResult).getReason().getStatusReason()); + public static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { + ServerAddress address = ServerAddress.fromString(serverIp); + ServerAddressResolveResult resolveResult = address.resolve(); + + if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { + return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); + } + + ServerAddressResolveResult.SuccessResult successResult = (ServerAddressResolveResult.SuccessResult) resolveResult; + ServerStatusResult modernStatusResult = getStatus(successResult); + if (modernStatusResult.isSuccess()) { + return modernStatusResult; + } + + Exception collectException = new IOException("Failed to get server status"); + collectException.addSuppressed(((ServerStatusResult.FailureResult) modernStatusResult).getException()); + LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); + } + + private static @NotNull ServerStatusResult getStatus(ServerAddressResolveResult.SuccessResult successResult) throws IOException { + try (Socket socket = new Socket()) { + socket.setOption(StandardSocketOptions.TCP_NODELAY, true); + socket.connect(successResult.getConnectAddress(), 7_000); + socket.setSoTimeout(7_000); + + try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + DataInputStream in = new DataInputStream(socket.getInputStream())) { + + sendHandshakeStatusPacket(out, + successResult.getPackerHandshakeAddress(), successResult.getPackerHandshakePort() + ); + sendStatusRequestPacket(out); + var status = readStatusResponsePacket(in); + + long sendTime = System.currentTimeMillis(); + sendPingRequestPacket(out); + var pong = readPongResponsePacket(in); + long networkLatency = System.currentTimeMillis() - sendTime; + + JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); + + ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); + return ServerStatusResult.success(parseServerStatus); } } catch (Exception e) { - LOG.error("Failed to get the status of server " + serverIp, e); - return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 5ee31b45413..308156b65b4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -24,8 +24,8 @@ public static ServerStatusResult success(ServerStatus serverStatus) { return new SuccessResult(serverStatus); } - public static ServerStatusResult failure(FailureResult.Reason reason) { - return new FailureResult(reason); + public static ServerStatusResult failure(FailureResult.Reason reason, Exception exception) { + return new FailureResult(reason, exception); } public @Nullable ServerStatus getIfSucceed() { @@ -59,10 +59,20 @@ private SuccessResult(ServerStatus serverStatus) { } public static final class FailureResult extends ServerStatusResult { - public final Reason reason; + private final Reason reason; + private final Exception exception; - private FailureResult(Reason reason) { + private FailureResult(Reason reason, Exception exception) { this.reason = reason; + this.exception = exception; + } + + public Reason getReason() { + return reason; + } + + public Exception getException() { + return exception; } public enum Reason { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java index 50bf0181795..f948e0b1af4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.server.resolver; +import org.jackhuang.hmcl.server.ServerBlockedByMojangException; + import java.io.IOException; import java.net.InetSocketAddress; import java.net.URLDecoder; @@ -26,18 +28,22 @@ import java.util.Map; public final class ServerAddress { + private final String rawServerIp; private final HostAndPort hostAndPort; private final Map queryProperties; private ServerAddress( + String rawServerIp, HostAndPort hostAndPort, Map queryProperties ) { + this.rawServerIp = rawServerIp; this.hostAndPort = hostAndPort; this.queryProperties = queryProperties; } public static ServerAddress fromString(String input) throws IOException { + String rawServerIp = input; if (input == null || input.isEmpty()) throw new IOException("server ip is empty."); @@ -85,25 +91,25 @@ public static ServerAddress fromString(String input) throws IOException { } catch (Exception ignore) { } } - return new ServerAddress(new HostAndPort(host, port), queryProperties); + return new ServerAddress(rawServerIp, new HostAndPort(host, port), queryProperties); } public ServerAddressResolveResult resolve() { try { InetSocketAddress inetSocketAddress = hostAndPort.toInetSocketAddress(); if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { - return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG); + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); } HostAndPort lookupDnsResult = ServerDnsSrvRedirector.lookup(this); if (lookupDnsResult != null) { inetSocketAddress = lookupDnsResult.toInetSocketAddress(); if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { - return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG); + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); } } return ServerAddressResolveResult.succeed(this, inetSocketAddress); } catch (UnknownHostException e) { - return ServerAddressResolveResult.failed(ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST); + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST, e); } } @@ -111,7 +117,11 @@ public Map getQueryProperties() { return queryProperties; } - public HostAndPort getHostAndIp() { + public HostAndPort getHostAndPort() { return hostAndPort; } + + public String getRawServerIp() { + return rawServerIp; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java index 93bd8114da2..55fe2da361d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -27,21 +27,22 @@ import java.util.Map; public sealed class ServerAddressResolveResult { + private final ServerAddress rawAddress; - private ServerAddressResolveResult() { - + private ServerAddressResolveResult(ServerAddress rawAddress) { + this.rawAddress = rawAddress; } - public static @NotNull ServerAddressResolveResult.FailureResult failed(FailureResult.Reason reason) { - return new FailureResult(reason); + public static @NotNull ServerAddressResolveResult.FailureResult failed(ServerAddress rawAddress, FailureResult.Reason reason, Exception exception) { + return new FailureResult(rawAddress, reason, exception); } - public static SuccessResult succeed(ServerAddress serverAddress, InetSocketAddress address) { - Map queryProperties = new LinkedHashMap<>(serverAddress.getQueryProperties()); - boolean isSame = serverAddress.getHostAndIp().equals(address.getHostName(), address.getPort()); + public static SuccessResult succeed(ServerAddress rawAddress, InetSocketAddress resolvedAddress) { + Map queryProperties = new LinkedHashMap<>(rawAddress.getQueryProperties()); + boolean isSame = rawAddress.getHostAndPort().equals(resolvedAddress.getHostName(), resolvedAddress.getPort()); if (!isSame || queryProperties.get("_o") != null) { - queryProperties.put("_o", serverAddress.getHostAndIp().host() + ":" + serverAddress.getHostAndIp().port()); + queryProperties.put("_o", rawAddress.getHostAndPort().host() + ":" + rawAddress.getHostAndPort().port()); } StringBuilder queryArgBuilder = new StringBuilder(); @@ -60,18 +61,29 @@ public static SuccessResult succeed(ServerAddress serverAddress, InetSocketAddre } if (queryArgBuilder.isEmpty()) { - queryArgBuilder.insert(0, address.getHostName()); + queryArgBuilder.insert(0, resolvedAddress.getHostName()); } else { - queryArgBuilder.insert(0, address.getHostName() + "?"); + queryArgBuilder.insert(0, resolvedAddress.getHostName() + "?"); } - return new SuccessResult(address, queryArgBuilder.toString(), address.getPort()); + return new SuccessResult(rawAddress, resolvedAddress, queryArgBuilder.toString(), resolvedAddress.getPort()); + } + + public ServerAddress getRawAddress() { + return rawAddress; } public static final class FailureResult extends ServerAddressResolveResult { private final Reason reason; + private final Exception exception; - public FailureResult(Reason reason) { + public FailureResult(ServerAddress rawAddress, Reason reason, Exception exception) { + super(rawAddress); this.reason = reason; + this.exception = exception; + } + + public Exception getException() { + return exception; } public Reason getReason() { @@ -96,7 +108,8 @@ public static final class SuccessResult extends ServerAddressResolveResult { private final String packerHandshakeAddress; private final int packerHandshakePort; - public SuccessResult(InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { + public SuccessResult(ServerAddress rawAddress, InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { + super(rawAddress); this.connectAddress = connectAddress; this.packerHandshakeAddress = packerHandshakeAddress; this.packerHandshakePort = packerHandshakePort; @@ -113,14 +126,5 @@ public String getPackerHandshakeAddress() { public int getPackerHandshakePort() { return packerHandshakePort; } - - @Override - public String toString() { - return "SuccessResult{" + - "connectAddress=" + connectAddress + - ", packerHandshakeAddress='" + packerHandshakeAddress + '\'' + - ", packerHandshakePort=" + packerHandshakePort + - '}'; - } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java index b5db5a82c9f..88b71861215 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java @@ -49,9 +49,9 @@ private ServerDnsSrvRedirector() { DirContext context = new InitialDirContext(env); redirector = (address) -> { - if (address.getHostAndIp().port() == 25565) { + if (address.getHostAndPort().port() == 25565) { try { - Attributes attributes = context.getAttributes("_minecraft._tcp." + address.getHostAndIp().host(), new String[]{"SRV"}); + Attributes attributes = context.getAttributes("_minecraft._tcp." + address.getHostAndPort().host(), new String[]{"SRV"}); Attribute srvAttribute = attributes.get("srv"); if (srvAttribute != null) { String[] arguments = srvAttribute.get().toString().split(" ", 4); From 53d6c4a533906e0173bbd58c0d53faae5daae13d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 9 Oct 2026 01:59:29 +0800 Subject: [PATCH 55/97] =?UTF-8?q?server=20status=20pane=20=E9=87=8D?= =?UTF-8?q?=E6=96=B0=E8=B0=83=E6=95=B4=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/ServerAddressMaskPane.java | 73 +++++----- .../ui/instances/server/ServerStatusPane.java | 136 ++++++++---------- HMCL/src/main/resources/assets/css/root.css | 61 -------- 3 files changed, 96 insertions(+), 174 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java index ad82443813f..63c846e6181 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java @@ -19,10 +19,9 @@ import javafx.scene.control.Label; import javafx.scene.control.Tooltip; +import javafx.scene.effect.BoxBlur; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; -import javafx.scene.layout.Region; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public class ServerAddressMaskPane extends HBox { @@ -34,60 +33,68 @@ public class ServerAddressMaskPane extends HBox { private final Tooltip queryLabelTooltip = new Tooltip(); public ServerAddressMaskPane(@Nullable String serverIP) { - queryLabel.setOpacity(0.3); - atLabel.setOpacity(0.3); - - queryLabel.setMinWidth(Region.USE_PREF_SIZE); - atLabel.setMinWidth(Region.USE_PREF_SIZE); - realServerLabel.setMinWidth(Region.USE_PREF_SIZE); - getChildren().add(atLabel); getChildren().add(realServerLabel); getChildren().add(queryLabel); - FXUtils.installFastTooltip(atLabel, atLabelTooltip); - FXUtils.installFastTooltip(queryLabel, queryLabelTooltip); + atLabel.setPickOnBounds(true); + queryLabel.setPickOnBounds(true); + +// FXUtils.installFastTooltip(atLabel, atLabelTooltip); +// FXUtils.installFastTooltip(queryLabel, queryLabelTooltip); set(serverIP); - } - public static void installMask(@NotNull Label label, Tooltip tooltip, String text) { - int length = text.length(); - StringBuilder maskBuilder = new StringBuilder(); - for (int i = 0; i < length; i++) { - char c = text.charAt(i); - if (c == '@' || c == '?') { - maskBuilder.append(c); - } else { - maskBuilder.append("*"); - } - } - String mask = maskBuilder.toString(); + BoxBlur blur = new BoxBlur(); + blur.setIterations(3); + + atLabel.setEffect(blur); + queryLabel.setEffect(blur); - label.setText(mask); - tooltip.setText(text); + atLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { + atLabel.setEffect(null); + }); + atLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { + atLabel.setEffect(blur); + }); + + queryLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { + queryLabel.setEffect(null); + }); + queryLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { + queryLabel.setEffect(blur); + }); } public void set(@Nullable String serverIP) { if (serverIP == null) { + atLabel.setText(""); realServerLabel.setText(""); - installMask(atLabel, atLabelTooltip, ""); - installMask(queryLabel, queryLabelTooltip, ""); + queryLabel.setText(""); + + atLabelTooltip.setText(""); + queryLabelTooltip.setText(""); return; } int queryStart = serverIP.lastIndexOf('?'); if (queryStart != -1) { - installMask(queryLabel, queryLabelTooltip, serverIP.substring(queryStart)); + String queryStr = serverIP.substring(queryStart); + queryLabel.setText(queryStr); + atLabelTooltip.setText(queryStr); serverIP = serverIP.substring(0, queryStart); } else { - installMask(queryLabel, queryLabelTooltip, ""); + queryLabel.setText(""); + atLabelTooltip.setText(""); } int atEnd = serverIP.indexOf('@'); if (atEnd != -1) { - installMask(atLabel, atLabelTooltip, serverIP.substring(0, atEnd + 1)); + String atStr = serverIP.substring(0, atEnd + 1); + atLabel.setText(atStr); + atLabelTooltip.setText(atStr); serverIP = serverIP.substring(atEnd + 1); } else { - installMask(atLabel, atLabelTooltip, ""); + atLabel.setText(""); + atLabelTooltip.setText(""); } realServerLabel.setText(serverIP); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index f7ddd72c199..b1adbba2ef1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -22,11 +22,12 @@ import com.jfoenix.controls.JFXDialogLayout; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.control.Separator; import javafx.scene.image.Image; -import javafx.scene.layout.GridPane; +import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; @@ -40,8 +41,8 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.MinecraftChatComponentUtils; +import org.jetbrains.annotations.Nullable; -import java.util.List; import java.util.Objects; import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; @@ -140,32 +141,6 @@ public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedS observableServerStatus.refreshIfNoResultOrFailedAsync(true); } - private static VBox createStatCell(String titleText, String valueText, boolean showDivider, String tooltip) { - VBox cell = new VBox(3); - cell.getStyleClass().add("server-status-stat-cell"); - if (!showDivider) { - cell.getStyleClass().add("server-status-stat-cell-last"); - } - cell.setAlignment(Pos.CENTER_LEFT); - cell.setPadding(new Insets(10, 12, 10, 12)); - cell.setPrefWidth(140); - - Label title = new Label(titleText); - title.getStyleClass().add("server-status-stat-title"); - - Label value = new Label(valueText == null || valueText.isBlank() ? "—" : valueText); - value.getStyleClass().add("server-status-stat-value"); - value.setWrapText(true); - value.setMaxWidth(100); - - if (tooltip != null) { - FXUtils.installFastTooltip(value, tooltip); - } - - cell.getChildren().addAll(title, value); - return cell; - } - public void applyPinging(boolean pinging) { if (pinging) { lblErrorMessage.setText(""); @@ -175,6 +150,18 @@ public void applyPinging(boolean pinging) { } } + private static Node createDescriptionRow(String title, String content, @Nullable String tooltip, Insets inVBoxInsets) { + BorderPane row = new BorderPane(); + VBox.setMargin(row, inVBoxInsets); + row.setLeft(new Label(title)); + Label right = new Label(content); + if (tooltip != null) { + FXUtils.installFastTooltip(right, tooltip); + } + row.setRight(right); + return row; + } + public void applyResult(ServerStatusResult statusResult) { ServerStatus status; @@ -199,66 +186,61 @@ public void applyResult(ServerStatusResult statusResult) { serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); } - VBox root = new VBox(); - root.setSpacing(10); - HBox serverInfoBox = new HBox(); - VBox.setMargin(serverInfoBox, new Insets(10, 0, 0, 5)); + VBox rootContent = new VBox(); + rootContent.setSpacing(10); + rootLayout.setBody(rootContent); + + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + titleContainer.setAlignment(Pos.CENTER_LEFT); + VBox.setMargin(titleContainer, new Insets(12, 0, 0, 0)); + rootContent.getChildren().add(titleContainer); ImageContainer imageView = new ImageContainer(64); imageView.setImage(serverIcon); + titleContainer.getChildren().add(imageView); - StackPane canvasPane = new StackPane(imageView); - canvasPane.setPrefWidth(64); - canvasPane.setPrefHeight(64); - serverInfoBox.getChildren().add(canvasPane); - - GridPane textPane = new GridPane(); - textPane.setAlignment(Pos.CENTER_LEFT); + VBox titleContent = new VBox(); + titleContent.setAlignment(Pos.CENTER_LEFT); + titleContent.setSpacing(8); + HBox.setMargin(titleContent, new Insets(0, 0, 0, 12)); + titleContainer.getChildren().add(titleContent); - textPane.setHgap(10); - textPane.setVgap(10); + TwoLineListItem title = new TwoLineListItem(); + title.getTitleLabel().setWrapText(true); + title.setTitle(iconedServer.getName()); + titleContent.getChildren().add(title); - serverInfoBox.getChildren().add(textPane); - HBox.setMargin(textPane, new Insets(0, 0, 0, 20)); + ServerAddressMaskPane addressMaskPane = new ServerAddressMaskPane(iconedServer.getIp()); + addressMaskPane.labelAddStyleClass("subtitle"); + titleContent.getChildren().add(addressMaskPane); - Label label = new Label(i18n("server.name") + ":"); - textPane.add(label, 0, 0); - textPane.add(new Label(iconedServer.getName()), 1, 0); - - textPane.add(new Label(i18n("server.ip") + ":"), 0, 1); - textPane.add(new ServerAddressMaskPane(iconedServer.getIp()), 1, 1); - - root.getChildren().add(serverInfoBox); if (status != null) { - VBox motdBox = new VBox(5); - motdBox.getStyleClass().add("server-status-motd-box"); - motdBox.setPadding(new Insets(10, 12, 10, 12)); - motdBox.setMaxWidth(620); + VBox motdContentBox = new VBox(); + motdContentBox.setSpacing(5); + motdContentBox.setAlignment(Pos.CENTER_LEFT); - Label motdTitle = new Label(i18n("server.motd")); - motdTitle.getStyleClass().add("server-status-motd-title"); - motdBox.getChildren().add(motdTitle); + VBox.setMargin(motdContentBox, new Insets(4, 0, 0, 4)); + rootContent.getChildren().add(motdContentBox); - String motd = MinecraftChatComponentUtils.toPlainStringFromChatComponent(status.description()); - String[] motdLines = motd.split("\\r?\\n"); + String[] motdLines = MinecraftChatComponentUtils + .toPlainStringFromChatComponent(status.description()) + .split("\\r?\\n"); for (int i = 0; i < Math.min(motdLines.length, 2); i++) { - String line = motdLines[i]; + String line = motdLines[i].trim(); if (line.length() > 80) { line = line.substring(0, 77).trim() + "..."; } Label lineLabel = new Label(line); - lineLabel.getStyleClass().add("server-status-motd-line"); lineLabel.setMaxWidth(560); lineLabel.setWrapText(true); - motdBox.getChildren().add(lineLabel); + motdContentBox.getChildren().add(lineLabel); } - root.getChildren().add(motdBox); - - HBox detailRow = new HBox(); - detailRow.getStyleClass().add("server-status-detail-row"); - detailRow.setSpacing(0); - detailRow.setMaxWidth(620); + Separator titleEndSeparator = new Separator(); + titleEndSeparator.setMaxWidth(Double.MAX_VALUE); + titleEndSeparator.setPadding(new Insets(4, 0, 0, 0)); + rootContent.getChildren().add(titleEndSeparator); String playersTooltip = null; if (!status.players().samples().isEmpty()) { @@ -298,18 +280,12 @@ public void applyResult(ServerStatusResult statusResult) { modsTooltip = sb.toString(); } - var cells = List.of( - createStatCell(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), true, modsTooltip), - createStatCell(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), true, playersTooltip), - createStatCell(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", true, null), - createStatCell(i18n("server.latency"), String.format("%,dms", status.networkLatency()), false, null) - ); - detailRow.getChildren().addAll(cells); - - root.getChildren().add(detailRow); + rootContent.getChildren().add(createDescriptionRow(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), modsTooltip, new Insets(0, 4, 0, 4))); + rootContent.getChildren().add(createDescriptionRow(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), playersTooltip, new Insets(0, 4, 0, 4))); + rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); + rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", status.networkLatency()), null, new Insets(0, 4, 0, 4))); } - rootLayout.setBody(root); } private void onClose() { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 3e7966baec7..634c20a7248 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -2014,64 +2014,3 @@ .addon-changelog-table-cell { -fx-text-fill: -monet-primary; } - - -/******************************************************************************* - * * - * Server Status * - * * - ******************************************************************************/ - -.server-status-motd-box { - -fx-background-color: -monet-surface-container; - -fx-background-radius: 10px; - -fx-border-color: -monet-outline-variant; - -fx-border-width: 1px; - -fx-border-radius: 10px; - -fx-max-width: 620px; -} - -.server-status-motd-title { - -fx-font-size: 12px; - -fx-font-weight: 700; - -fx-text-fill: -monet-on-surface; -} - -.server-status-motd-line { - -fx-font-size: 13px; - -fx-text-fill: -monet-on-surface; - -fx-wrap-text: true; - -fx-max-width: 560px; -} - -.server-status-detail-row { - -fx-background-color: -monet-surface-container-low; - -fx-background-radius: 10px; - -fx-border-color: -monet-outline-variant; - -fx-border-width: 1px; - -fx-border-radius: 10px; - -fx-max-width: 620px; -} - -.server-status-stat-cell { - -fx-border-color: -monet-outline-variant; - -fx-border-width: 0 1 0 0; -} - -.server-status-stat-cell-last { - -fx-border-width: 0; -} - -.server-status-stat-title { - -fx-font-size: 12px; - -fx-font-weight: 600; - -fx-text-fill: -monet-on-surface-variant; -} - -.server-status-stat-value { - -fx-font-size: 13px; - -fx-font-weight: 500; - -fx-text-fill: -monet-on-surface; - -fx-wrap-text: true; - -fx-max-width: 100px; -} \ No newline at end of file From b6ec4ee2b4817c6d73463ed9efc218a878087ed2 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 9 Oct 2026 18:47:27 +0800 Subject: [PATCH 56/97] update ServerListPage --- .../org/jackhuang/hmcl/ui/instances/server/ServerListPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index ea401ae212b..d90f3a78bba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -478,7 +478,6 @@ public ServerListCell(ServerListPage page) { HBox contentLine2 = new HBox(contentLine2AddressMaskPane); VBox center = new VBox(contentLine1, contentLine2); - center.setMouseTransparent(true); root.setCenter(center); } From f379b7ee64cccc7ddcb56d89a0d61bd3a93abe74 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 9 Oct 2026 20:36:10 +0800 Subject: [PATCH 57/97] Legacy ping support --- .../ui/instances/server/EditServerPane.java | 2 +- .../server/ObservableServerStatus.java | 2 +- .../ui/instances/server/ServerStatusPane.java | 4 +- .../org/jackhuang/hmcl/server/ServerDnD.java | 2 +- .../jackhuang/hmcl/server/ServerStatus.java | 2 +- .../pinger/LegacyServerStatusPinger.java | 158 ++++++++++++++++++ .../ModernServerStatusPinger.java} | 76 +++------ .../server/pinger/ServerStatusPinger.java | 77 +++++++++ ...rAddress.java => ModernServerAddress.java} | 8 +- .../resolver/ServerAddressResolveResult.java | 14 +- .../resolver/ServerDnsSrvRedirector.java | 4 +- 11 files changed, 278 insertions(+), 71 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{ServerStatusPinger.java => pinger/ModernServerStatusPinger.java} (73%) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/{ServerAddress.java => ModernServerAddress.java} (94%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 6714f2b7b95..f3f78aa5996 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -31,8 +31,8 @@ import javafx.scene.layout.*; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusPinger; import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.TransitionPane; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java index d0e087c6a7d..585d7a3d8ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java @@ -21,8 +21,8 @@ import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.ReadOnlyObjectWrapper; -import org.jackhuang.hmcl.server.ServerStatusPinger; import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index b1adbba2ef1..0c88385740c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -282,7 +282,9 @@ public void applyResult(ServerStatusResult statusResult) { rootContent.getChildren().add(createDescriptionRow(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), modsTooltip, new Insets(0, 4, 0, 4))); rootContent.getChildren().add(createDescriptionRow(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), playersTooltip, new Insets(0, 4, 0, 4))); - rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); + if (status.version() != null) { + rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); + } rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", status.networkLatency()), null, new Insets(0, 4, 0, 4))); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java index a9401aecc93..e3c1d273e43 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -46,7 +46,7 @@ * Optional fields: *
    *
  • {@code favicon}: the server icon. This value should be the raw PNG base64 payload only, without - * the {@code data:image/png;base64,} prefix. The parser in {@link ServerStatusPinger} accepts the + * the {@code data:image/png;base64,} prefix. The parser in {@link org.jackhuang.hmcl.server.pinger.ServerStatusPinger} accepts the * Minecraft status format where the remote payload may include a data URL, but it strips the prefix and * keeps only the base64 bytes. This drag-and-drop payload therefore stores the base64 text directly, * not the full data URL.
  • diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 5fa138928e7..0e178162dcb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -26,7 +26,7 @@ public record ServerStatus( long networkLatency, - @NotNull Version version, + @Nullable Version version, @NotNull Players players, @NotNull JsonElement description, @Nullable String favicon, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java new file mode 100644 index 00000000000..7639819c6dc --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -0,0 +1,158 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.pinger; + +import com.google.gson.JsonPrimitive; +import org.jackhuang.hmcl.server.ServerStatus; +import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.net.StandardSocketOptions; +import java.util.List; + +// https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#1.6 +final class LegacyServerStatusPinger implements ServerStatusPinger { + static LegacyServerStatusPinger instance = new LegacyServerStatusPinger(); + + private LegacyServerStatusPinger() { + + } + + @Override + public @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) throws IOException { + try (Socket socket = new Socket()) { + socket.setOption(StandardSocketOptions.TCP_NODELAY, true); + socket.connect(successResult.getConnectAddress(), 7_000); + socket.setSoTimeout(7_000); + + try (DataOutputStream out = new DataOutputStream(socket.getOutputStream()); + DataInputStream in = new DataInputStream(socket.getInputStream())) { + long sendTime = System.currentTimeMillis(); + + out.writeByte(0xfe); // packet id + out.writeByte(0x01); // payload(always 1) + out.writeByte(0xfa); // plugin message + + writeString(out, "MC|PingHost"); // magic + + byte[] remainBytes = toByteArray(remainOut -> { + remainOut.writeByte(0x7f); // protocol version + writeString(remainOut, successResult.getRawAddress().getHostAndPort().host()); // raw host + remainOut.writeInt(successResult.getRawAddress().getHostAndPort().port()); // raw port + }); + + out.writeShort(remainBytes.length); // remain data length + out.write(remainBytes); // remain data + + int receivePacketId = in.readUnsignedByte(); + if (receivePacketId != 0xff) { + throw new IOException("Invalid packet id " + receivePacketId + ", expected 0xff(Kick Packet)."); + } + + String response = readString(in); + long networkLatency = System.currentTimeMillis() - sendTime; + return ServerStatusResult.success(parseResult(networkLatency, response)); + } + } + } + + private @NotNull ServerStatus parseResult(long networkLatency, @NotNull String input) throws IOException { + ServerStatus.Version version; + String motd; + ServerStatus.Players players; + try { + if (input.startsWith("§")) { + String[] strings = input.split("\0"); + + if (strings[0].equals("§1")) { + version = new ServerStatus.Version( + strings[2], + parseIntOrDefault(strings[1], 0) + ); + motd = strings[3]; + + players = new ServerStatus.Players( + parseIntOrDefault(strings[5], -1), + parseIntOrDefault(strings[4], -1), + List.of() + ); + } else { + throw new IOException("Invalid header: " + strings[0]); + } + } else { + String[] strings = input.split("§"); + motd = strings[0]; + + version = null; + players = new ServerStatus.Players( + parseIntOrDefault(strings[2], -1), + parseIntOrDefault(strings[1], -1), + List.of() + ); + } + + return new ServerStatus( + networkLatency, + version, + players, + new JsonPrimitive(motd), null, false, null); + } catch (Exception e) { + throw new IOException("Failed to parse legacy server status: " + input, e); + } + } + + private int parseIntOrDefault(@NotNull String input, int defaultValue) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + return defaultValue; + } + } + + private void writeString(final DataOutputStream out, final String str) throws IOException { + out.writeShort(str.length()); + + for (char c : str.toCharArray()) { + out.writeChar(c); + } + } + + private String readString(final DataInputStream in) throws IOException { + int length = in.readShort(); + + char[] chars = new char[length]; + for (int i = 0; i < length; i++) { + chars[i] = in.readChar(); + } + return new String(chars); + } + + private byte[] toByteArray(@NotNull DataWriter writer) throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream(); + DataOutputStream dataOut = new DataOutputStream(out)) { + writer.write(dataOut); + return out.toByteArray(); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java similarity index 73% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java index 8051b6ad923..30f77cc52ae 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java @@ -15,10 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server; +package org.jackhuang.hmcl.server.pinger; import com.google.gson.*; -import org.jackhuang.hmcl.server.resolver.ServerAddress; +import org.jackhuang.hmcl.server.ServerStatus; +import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; import org.jetbrains.annotations.NotNull; @@ -30,37 +31,16 @@ import java.util.Objects; import java.util.UUID; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping -// https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#1.6 // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Minecraft_Forge_Handshake -public final class ServerStatusPinger { - private ServerStatusPinger() { - - } +final class ModernServerStatusPinger implements ServerStatusPinger { + static ModernServerStatusPinger instance = new ModernServerStatusPinger(); - public static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { - ServerAddress address = ServerAddress.fromString(serverIp); - ServerAddressResolveResult resolveResult = address.resolve(); + private ModernServerStatusPinger() { - if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { - return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); - } - - ServerAddressResolveResult.SuccessResult successResult = (ServerAddressResolveResult.SuccessResult) resolveResult; - ServerStatusResult modernStatusResult = getStatus(successResult); - if (modernStatusResult.isSuccess()) { - return modernStatusResult; - } - - Exception collectException = new IOException("Failed to get server status"); - collectException.addSuppressed(((ServerStatusResult.FailureResult) modernStatusResult).getException()); - LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); - return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); } - private static @NotNull ServerStatusResult getStatus(ServerAddressResolveResult.SuccessResult successResult) throws IOException { + public @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) { try (Socket socket = new Socket()) { socket.setOption(StandardSocketOptions.TCP_NODELAY, true); socket.connect(successResult.getConnectAddress(), 7_000); @@ -90,7 +70,7 @@ private ServerStatusPinger() { } } - private static ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatus) { + private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatus) { JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); ServerStatus.Version version = new ServerStatus.Version(versionJsonObject.get("name").getAsString(), versionJsonObject.get("protocol").getAsInt()); @@ -159,7 +139,7 @@ private static ServerStatus parseServerStatus(long networkLatency, JsonObject ro ); } - private static long readPongResponsePacket(DataInputStream in) throws IOException { + private long readPongResponsePacket(DataInputStream in) throws IOException { return readPacket(in, packetIn -> { int packetId = readVarInt(packetIn); if (packetId != 0x01) @@ -168,7 +148,7 @@ private static long readPongResponsePacket(DataInputStream in) throws IOExceptio }); } - private static T readPacket(DataInputStream in, PacketReader reader) throws IOException { + private T readPacket(DataInputStream in, DataReader reader) throws IOException { int length = readVarInt(in); if (length > 2097151) throw new IOException("Packet length too large: " + length); byte[] packetData = new byte[length]; @@ -176,11 +156,11 @@ private static T readPacket(DataInputStream in, PacketReader reader) thro try (ByteArrayInputStream bais = new ByteArrayInputStream(packetData); DataInputStream packetIn = new DataInputStream(bais)) { - return reader.reader(packetIn); + return reader.read(packetIn); } } - private static String readStatusResponsePacket(DataInputStream in) throws IOException { + private String readStatusResponsePacket(DataInputStream in) throws IOException { return readPacket(in, packetIn -> { int packetId = readVarInt(packetIn); if (packetId != 0x00) @@ -190,23 +170,23 @@ private static String readStatusResponsePacket(DataInputStream in) throws IOExce }); } - private static void sendStatusRequestPacket(DataOutputStream sendTarget) throws IOException { + private void sendStatusRequestPacket(DataOutputStream sendTarget) throws IOException { sendPacket(sendTarget, out -> { out.writeByte(0x00); // Packet ID }); } - private static void sendPacket(DataOutputStream target, PacketWriter packetWriter) throws IOException { + private void sendPacket(DataOutputStream target, DataWriter dataWriter) throws IOException { try (ByteArrayOutputStream packetOut = new ByteArrayOutputStream(); DataOutputStream dataPacketOut = new DataOutputStream(packetOut)) { - packetWriter.write(dataPacketOut); + dataWriter.write(dataPacketOut); writeVarInt(target, packetOut.size()); // Packet Size target.write(packetOut.toByteArray()); // Packet Contents } } - private static void sendHandshakeStatusPacket(DataOutputStream sendTarget, String address, int port) throws IOException { + private void sendHandshakeStatusPacket(DataOutputStream sendTarget, String address, int port) throws IOException { sendPacket(sendTarget, out -> { // Packet ID out.write(0x00); @@ -225,7 +205,7 @@ private static void sendHandshakeStatusPacket(DataOutputStream sendTarget, Strin }); } - private static void sendPingRequestPacket(DataOutputStream sendTarget) throws IOException { + private void sendPingRequestPacket(DataOutputStream sendTarget) throws IOException { sendPacket(sendTarget, out -> { out.write(0x01); // packet id long time = System.currentTimeMillis(); @@ -234,7 +214,7 @@ private static void sendPingRequestPacket(DataOutputStream sendTarget) throws IO }); } - private static void writeVarInt(OutputStream out, int value) throws IOException { + private void writeVarInt(OutputStream out, int value) throws IOException { while ((value & ~0x7F) != 0) { out.write((value & 0x7F) | 0x80); @@ -244,7 +224,7 @@ private static void writeVarInt(OutputStream out, int value) throws IOException out.write(value); } - private static String readVarString(DataInputStream in) throws IOException { + private String readVarString(DataInputStream in) throws IOException { int length = readVarInt(in); if (length > 32768) throw new IOException("Packet string length too large: " + length); byte[] bytes = new byte[length]; @@ -252,7 +232,7 @@ private static String readVarString(DataInputStream in) throws IOException { return new String(bytes, StandardCharsets.UTF_8); } - private static int readVarInt(InputStream in) throws IOException { + private int readVarInt(InputStream in) throws IOException { int value = 0; int position = 0; int currentByte; @@ -273,19 +253,19 @@ private static int readVarInt(InputStream in) throws IOException { throw new IOException("VarInt too big"); } - private static void writeLong(OutputStream out, long value) throws IOException { + private void writeLong(OutputStream out, long value) throws IOException { for (int i = 7; i >= 0; i--) { out.write((int) ((value >>> (i * 8)) & 0xFF)); } } - private static void writeVarString(OutputStream out, String string) throws IOException { + private void writeVarString(OutputStream out, String string) throws IOException { byte[] bytes = string.getBytes(StandardCharsets.UTF_8); writeVarInt(out, bytes.length); out.write(bytes); } - private static long readLong(InputStream in) throws IOException { + private long readLong(InputStream in) throws IOException { long value = 0; for (int i = 0; i < 8; i++) { int b = in.read(); @@ -296,14 +276,4 @@ private static long readLong(InputStream in) throws IOException { } return value; } - - @FunctionalInterface - private interface PacketWriter { - void write(DataOutputStream out) throws IOException; - } - - @FunctionalInterface - private interface PacketReader { - T reader(DataInputStream in) throws IOException; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java new file mode 100644 index 00000000000..6b249c5b31f --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -0,0 +1,77 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.pinger; + +import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.resolver.ModernServerAddress; +import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; +import org.jetbrains.annotations.NotNull; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +public interface ServerStatusPinger { + List PINGERS = List.of( + ModernServerStatusPinger.instance, + LegacyServerStatusPinger.instance + ); + + static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { + ModernServerAddress address = ModernServerAddress.fromString(serverIp); + ServerAddressResolveResult resolveResult = address.resolve(); + + if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { + return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); + } + + ServerAddressResolveResult.SuccessResult successResult = (ServerAddressResolveResult.SuccessResult) resolveResult; + List failures = new ArrayList<>(); + for (ServerStatusPinger pinger : PINGERS) { + ServerStatusResult statusResult = pinger.getStatus(successResult); + if (statusResult.isSuccess()) { + return statusResult; + } else { + failures.add((ServerStatusResult.FailureResult) statusResult); + } + } + + Exception collectException = new IOException("Failed to get server status"); + for (ServerStatusResult.FailureResult failureResult : failures) { + collectException.addSuppressed(failureResult.getException()); + } + LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); + } + + @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) throws IOException; + + @FunctionalInterface + interface DataWriter { + void write(DataOutputStream out) throws IOException; + } + + @FunctionalInterface + interface DataReader { + T read(DataInputStream in) throws IOException; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java similarity index 94% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java index f948e0b1af4..844aca887ac 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java @@ -27,12 +27,12 @@ import java.util.LinkedHashMap; import java.util.Map; -public final class ServerAddress { +public final class ModernServerAddress { private final String rawServerIp; private final HostAndPort hostAndPort; private final Map queryProperties; - private ServerAddress( + private ModernServerAddress( String rawServerIp, HostAndPort hostAndPort, Map queryProperties @@ -42,7 +42,7 @@ private ServerAddress( this.queryProperties = queryProperties; } - public static ServerAddress fromString(String input) throws IOException { + public static ModernServerAddress fromString(String input) throws IOException { String rawServerIp = input; if (input == null || input.isEmpty()) throw new IOException("server ip is empty."); @@ -91,7 +91,7 @@ public static ServerAddress fromString(String input) throws IOException { } catch (Exception ignore) { } } - return new ServerAddress(rawServerIp, new HostAndPort(host, port), queryProperties); + return new ModernServerAddress(rawServerIp, new HostAndPort(host, port), queryProperties); } public ServerAddressResolveResult resolve() { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java index 55fe2da361d..84c9a4067ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -27,17 +27,17 @@ import java.util.Map; public sealed class ServerAddressResolveResult { - private final ServerAddress rawAddress; + private final ModernServerAddress rawAddress; - private ServerAddressResolveResult(ServerAddress rawAddress) { + private ServerAddressResolveResult(ModernServerAddress rawAddress) { this.rawAddress = rawAddress; } - public static @NotNull ServerAddressResolveResult.FailureResult failed(ServerAddress rawAddress, FailureResult.Reason reason, Exception exception) { + public static @NotNull ServerAddressResolveResult.FailureResult failed(ModernServerAddress rawAddress, FailureResult.Reason reason, Exception exception) { return new FailureResult(rawAddress, reason, exception); } - public static SuccessResult succeed(ServerAddress rawAddress, InetSocketAddress resolvedAddress) { + public static SuccessResult succeed(ModernServerAddress rawAddress, InetSocketAddress resolvedAddress) { Map queryProperties = new LinkedHashMap<>(rawAddress.getQueryProperties()); boolean isSame = rawAddress.getHostAndPort().equals(resolvedAddress.getHostName(), resolvedAddress.getPort()); @@ -68,7 +68,7 @@ public static SuccessResult succeed(ServerAddress rawAddress, InetSocketAddress return new SuccessResult(rawAddress, resolvedAddress, queryArgBuilder.toString(), resolvedAddress.getPort()); } - public ServerAddress getRawAddress() { + public ModernServerAddress getRawAddress() { return rawAddress; } @@ -76,7 +76,7 @@ public static final class FailureResult extends ServerAddressResolveResult { private final Reason reason; private final Exception exception; - public FailureResult(ServerAddress rawAddress, Reason reason, Exception exception) { + public FailureResult(ModernServerAddress rawAddress, Reason reason, Exception exception) { super(rawAddress); this.reason = reason; this.exception = exception; @@ -108,7 +108,7 @@ public static final class SuccessResult extends ServerAddressResolveResult { private final String packerHandshakeAddress; private final int packerHandshakePort; - public SuccessResult(ServerAddress rawAddress, InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { + public SuccessResult(ModernServerAddress rawAddress, InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { super(rawAddress); this.connectAddress = connectAddress; this.packerHandshakeAddress = packerHandshakeAddress; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java index 88b71861215..3c840d09a72 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java @@ -35,7 +35,7 @@ private ServerDnsSrvRedirector() { } - static @Nullable HostAndPort lookup(ServerAddress address0) { + static @Nullable HostAndPort lookup(ModernServerAddress address0) { if (redirector == null) { synchronized (ServerDnsSrvRedirector.class) { if (redirector == null) { @@ -81,6 +81,6 @@ private ServerDnsSrvRedirector() { @FunctionalInterface private interface DnsSrvRedirector { - @Nullable HostAndPort lookup(@NotNull ServerAddress address); + @Nullable HostAndPort lookup(@NotNull ModernServerAddress address); } } From f7bfff47b76ea826144d61384774e21e2999b451 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Fri, 9 Oct 2026 20:53:02 +0800 Subject: [PATCH 58/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20Legacy=20ping=20?= =?UTF-8?q?=E6=9C=AA=E6=8D=95=E8=8E=B7=E6=8A=A5=E9=94=99=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E4=B8=80=E7=B3=BB=E5=88=97=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/server/pinger/LegacyServerStatusPinger.java | 4 +++- .../org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index 7639819c6dc..d1844367c76 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -40,7 +40,7 @@ private LegacyServerStatusPinger() { } @Override - public @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) throws IOException { + public @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) { try (Socket socket = new Socket()) { socket.setOption(StandardSocketOptions.TCP_NODELAY, true); socket.connect(successResult.getConnectAddress(), 7_000); @@ -74,6 +74,8 @@ private LegacyServerStatusPinger() { long networkLatency = System.currentTimeMillis() - sendTime; return ServerStatusResult.success(parseResult(networkLatency, response)); } + } catch (Exception e) { + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 6b249c5b31f..024047109b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -63,7 +63,7 @@ public interface ServerStatusPinger { return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); } - @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult) throws IOException; + @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult); @FunctionalInterface interface DataWriter { From ccf0787a7a09dbaa599227ff7d76e6a5f181f042 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 00:11:59 +0800 Subject: [PATCH 59/97] =?UTF-8?q?ping=E5=8D=8F=E8=AE=AE=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E5=88=B0b1.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pinger/LegacyServerStatusPinger.java | 31 +++++++++---------- .../pinger/ModernServerStatusPinger.java | 1 + .../server/pinger/ServerStatusPinger.java | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index d1844367c76..660ccf180b3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -32,6 +32,7 @@ import java.util.List; // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#1.6 +// Minecraft Beta 1.8 to Minecraft 1.6 final class LegacyServerStatusPinger implements ServerStatusPinger { static LegacyServerStatusPinger instance = new LegacyServerStatusPinger(); @@ -54,16 +55,16 @@ private LegacyServerStatusPinger() { out.writeByte(0x01); // payload(always 1) out.writeByte(0xfa); // plugin message - writeString(out, "MC|PingHost"); // magic - - byte[] remainBytes = toByteArray(remainOut -> { - remainOut.writeByte(0x7f); // protocol version - writeString(remainOut, successResult.getRawAddress().getHostAndPort().host()); // raw host - remainOut.writeInt(successResult.getRawAddress().getHostAndPort().port()); // raw port - }); - - out.writeShort(remainBytes.length); // remain data length - out.write(remainBytes); // remain data +// writeString(out, "MC|PingHost"); // magic +// +// byte[] remainBytes = toByteArray(remainOut -> { +// remainOut.writeByte(0x7f); // protocol version +// writeString(remainOut, successResult.getRawAddress().getHostAndPort().host()); // raw host +// remainOut.writeInt(successResult.getRawAddress().getHostAndPort().port()); // raw port +// }); +// +// out.writeShort(remainBytes.length); // remain data length +// out.write(remainBytes); // remain data int receivePacketId = in.readUnsignedByte(); if (receivePacketId != 0xff) { @@ -134,19 +135,17 @@ private int parseIntOrDefault(@NotNull String input, int defaultValue) { private void writeString(final DataOutputStream out, final String str) throws IOException { out.writeShort(str.length()); - - for (char c : str.toCharArray()) { - out.writeChar(c); - } + out.writeChars(str); } private String readString(final DataInputStream in) throws IOException { int length = in.readShort(); char[] chars = new char[length]; - for (int i = 0; i < length; i++) { - chars[i] = in.readChar(); + for (int index = 0; index < length; index++) { + chars[index] = in.readChar(); } + return new String(chars); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java index 30f77cc52ae..27381e15ffb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java @@ -33,6 +33,7 @@ // https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping // https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Minecraft_Forge_Handshake +// Minecraft 1.7+ final class ModernServerStatusPinger implements ServerStatusPinger { static ModernServerStatusPinger instance = new ModernServerStatusPinger(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 024047109b5..5c6035d5cf8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -32,7 +32,7 @@ public interface ServerStatusPinger { List PINGERS = List.of( - ModernServerStatusPinger.instance, +// ModernServerStatusPinger.instance, LegacyServerStatusPinger.instance ); From fd3396bd20e42582d97051beca8a532fbbe32f88 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 00:14:46 +0800 Subject: [PATCH 60/97] =?UTF-8?q?ping=E5=8D=8F=E8=AE=AE=E5=90=91=E4=B8=8B?= =?UTF-8?q?=E5=85=BC=E5=AE=B9=E5=88=B0b1.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 5c6035d5cf8..024047109b5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -32,7 +32,7 @@ public interface ServerStatusPinger { List PINGERS = List.of( -// ModernServerStatusPinger.instance, + ModernServerStatusPinger.instance, LegacyServerStatusPinger.instance ); From ae6d926167a80cd126ef76a743378311cc062f3b Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 00:34:20 +0800 Subject: [PATCH 61/97] =?UTF-8?q?=E6=98=BE=E7=A4=BA=E6=9C=AA=E7=9F=A5?= =?UTF-8?q?=E7=9A=84=E5=8D=8F=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/server/ServerStatusPane.java | 2 ++ HMCL/src/main/resources/assets/lang/I18N.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ar.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_de.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_es.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ja.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_lzh.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_ru.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_uk.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties | 1 + HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties | 1 + 11 files changed, 12 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 0c88385740c..e5f345f49c4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -284,6 +284,8 @@ public void applyResult(ServerStatusResult statusResult) { rootContent.getChildren().add(createDescriptionRow(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), playersTooltip, new Insets(0, 4, 0, 4))); if (status.version() != null) { rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); + } else { + rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), i18n("server.playversion.unknown"), null, new Insets(0, 4, 0, 4))); } rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", status.networkLatency()), null, new Insets(0, 4, 0, 4))); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 92c49d0a5cb..efd00917323 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1416,6 +1416,7 @@ server.name=Server name server.name.def=Minecraft Server server.onlineplayers=Online server.playversion=Version +server.playversion.unknown=Unknown server.tag.hide=Hidden server.tag.hold.current=Current server.type=Server type diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 2c2fc44ecdf..defb6f6ac97 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1416,6 +1416,7 @@ server.name=اسم الخادم server.name.def=Minecraft Server server.onlineplayers=على الانترنت server.playversion=الإصدار +server.playversion.unknown=غير معروف server.tag.hide=مخفي server.tag.hold.current=المحمل server.type=نوع الخادم diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 52582e7fb7a..c60470e8676 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1415,6 +1415,7 @@ server.name=Name des Servers server.name.def=Minecraft Server server.onlineplayers=Online server.playversion=Version +server.playversion.unknown=Unbekannt server.tag.hide=Versteckt server.tag.hold.current=Aktuell server.type=Servertyp diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 635c4f01171..241911a3fc1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1408,6 +1408,7 @@ server.name=Serveri nimi server.name.def=Minecraft Server server.onlineplayers=Internetis server.playversion=Versión +server.playversion.unknown=Desconocido server.tag.hide=Oculto server.tag.hold.current=PoseídoActual server.type=Tipo de servidor diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index da942e99936..fc863fe4f7a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1212,6 +1212,7 @@ server.name=サーバ名 server.name.def=Minecraft Server server.onlineplayers=オンライン server.playversion=バージョン +server.playversion.unknown=不明 server.tag.hide=隠しの server.tag.hold.current=保有中 server.type=サーバーの種類 diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 5113a955733..bb556d3f164 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1224,6 +1224,7 @@ selector.custom=自定 # server.name.def= # server.onlineplayers= # server.playversion= +# server.playversion.unknown= # server.tag.hide= # server.tag.hold.current= # server.type= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index d6a38f3cda0..5ba8344276c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1348,6 +1348,7 @@ server.name=Имя сервера server.name.def=Minecraft Server server.onlineplayers=Онлайн server.playversion=Версия +server.playversion.unknown=Неизвестно server.tag.hide=Скрытое server.tag.hold.current=ВладёныйТекущий server.type=Тип сервера diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index e2378f50dc9..bff873980b3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1371,6 +1371,7 @@ server.name=Назва сервера server.name.def=Minecraft Server server.onlineplayers=Онлайн server.playversion=Версія +server.playversion.unknown=Невідомо server.tag.hide=Приховане server.tag.hold.current=ВласнийПоточний server.type=Тип сервера 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 4dce9262778..2c23b13aa33 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1228,6 +1228,7 @@ server.name=服务器名字 server.name.def=Minecraft 服务器 server.onlineplayers=在线人数 server.playversion=版本 +server.playversion.unknown=未知 server.tag.hide=隐藏 server.tag.hold.current=当前实例 server.type=服务器类型 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 dba34bc3263..b83f422d2b6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1222,6 +1222,7 @@ server.name=伺服器名稱 server.name.def=Minecraft 伺服器 server.onlineplayers=線上人數 server.playversion=版本 +server.playversion.unknown=未知 server.tag.hide=隱藏 server.tag.hold.current=目前實例 server.type=伺服器類型 From 9330b9077daf45516e06d36c0acc9826cabf5bb0 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 02:06:29 +0800 Subject: [PATCH 62/97] =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=A1=B5=E5=8A=A0=E4=B8=8A=E6=90=9C=E7=B4=A2=E6=A1=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 195 +++++++++++++++--- 1 file changed, 163 insertions(+), 32 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index d90f3a78bba..5583c00b1b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -17,26 +17,22 @@ */ package org.jackhuang.hmcl.ui.instances.server; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXCheckBox; -import com.jfoenix.controls.JFXListView; -import com.jfoenix.controls.JFXPopup; +import com.jfoenix.controls.*; +import javafx.animation.PauseTransition; +import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.ListCell; -import javafx.scene.control.Skin; -import javafx.scene.control.Tooltip; +import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.input.MouseButton; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; +import javafx.scene.layout.*; +import javafx.util.Duration; import javafx.util.Subscription; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; @@ -45,8 +41,11 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; +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.instances.Instances; +import org.jackhuang.hmcl.util.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -56,10 +55,12 @@ import java.nio.file.Path; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; import java.util.function.UnaryOperator; -import static org.jackhuang.hmcl.ui.FXUtils.determineOptimalPopupPosition; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; +import static org.jackhuang.hmcl.ui.FXUtils.*; +import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; import static org.jackhuang.hmcl.util.StringUtils.parseColorEscapes; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; @@ -695,37 +696,167 @@ public void showPopupMenu(ServerListPage.ServerListItem holder, JFXPopup.PopupHP } } - private final class ServerListPageSkin extends ToolbarListPageSkin { + private final class ServerListPageSkin extends SkinBase { + private final JFXListView listView = new JFXListView<>(); + private final JFXTextField searchField = new JFXTextField(); + private final BooleanProperty isSearching = new SimpleBooleanProperty(false); + private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); + + private final TransitionPane toolbarPane = new TransitionPane(); + + private final HBox searchBar = new HBox(); + private final HBox toolbarNormal = new HBox(); ServerListPageSkin() { super(ServerListPage.this); + StackPane rootStackPane = new StackPane(); + getChildren().setAll(rootStackPane); + rootStackPane.setPadding(new Insets(10)); + rootStackPane.getStyleClass().addAll("notice-pane"); + + ComponentList root = new ComponentList(); + rootStackPane.getChildren().setAll(root); + root.getStyleClass().add("no-padding"); + + listView.getStyleClass().add("no-horizontal-scrollbar"); + listView.setCellFactory(param -> new ServerListCell(ServerListPage.this)); + listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); + + { + root.getContent().add(toolbarPane); + FXUtils.setOverflowHidden(toolbarPane, 8); + + Insets toolbarPadding = new Insets(0, 5, 0, 5); + + toolbarNormal.setAlignment(Pos.CENTER_LEFT); + toolbarNormal.setPadding(toolbarPadding); + toolbarNormal.getChildren().setAll( + createCheckBox(box -> { + box.setText(i18n("server.manage.show_all")); + box.selectedProperty().bindBidirectional(ServerListPage.this.showAll); + }), + createCheckBox(box -> { + box.setText(i18n("server.manage.show_hide")); + box.selectedProperty().bindBidirectional(ServerListPage.this.showHide); + }), + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, ServerListPage.this::refresh), + createToolbarButton2(i18n("server.manage.add"), SVG.ADD, ServerListPage.this::addServer), + createToolbarButton2(i18n("search"), SVG.SEARCH, () -> changeToolbar(searchBar)) + ); + + + searchBar.setAlignment(Pos.CENTER); + searchBar.setPadding(toolbarPadding); + searchField.setPromptText(i18n("search")); + HBox.setHgrow(searchField, Priority.ALWAYS); + searchPause.setOnFinished(e -> search()); + searchField.textProperty().addListener((observable, oldValue, newValue) -> { + if (isSearching.get() || !StringUtils.isBlank(newValue)) { + searchPause.setRate(1); + searchPause.playFromStart(); + } + }); + + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { + changeToolbar(toolbarNormal); + searchField.clear(); + searchPause.stop(); + isSearching.set(false); + Bindings.bindContent(listView.getItems(), ServerListPage.this.getItems()); + }); + + onEscPressed(searchField, closeSearchBar::fire); + + searchBar.getChildren().setAll( + createCheckBox(box -> { + box.setText(i18n("server.manage.show_all")); + box.selectedProperty().bindBidirectional(ServerListPage.this.showAll); + }), + createCheckBox(box -> { + box.setText(i18n("server.manage.show_hide")); + box.selectedProperty().bindBidirectional(ServerListPage.this.showHide); + }), + createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, ServerListPage.this::refresh), + searchField, + closeSearchBar + ); + toolbarPane.setContent(toolbarNormal, ContainerAnimations.NONE); + } + StackPane placeholderContainer = new StackPane(); placeholderContainer.getStyleClass().add("notice-pane"); Label placeholderLabel = new Label(i18n("server.empty")); + placeholderLabel.textProperty().bind(Bindings.createStringBinding(() -> { + if (isSearching.get()) { + return i18n("search.no_results_found"); + } else { + return i18n("server.empty"); + } + }, isSearching)); placeholderContainer.getChildren().add(placeholderLabel); listView.setPlaceholder(placeholderContainer); + + Bindings.bindContent(listView.getItems(), ServerListPage.this.getItems()); + ServerListPage.this.getItems().addListener((ListChangeListener) c -> { + if (isSearching.get()) { + search(); + } + }); + + SpinnerPane center = new SpinnerPane(); + ComponentList.setVgrow(center, Priority.ALWAYS); + center.loadingProperty().bind(ServerListPage.this.loadingProperty()); + + center.setContent(listView); + root.getContent().add(center); } - @Override - protected List initializeToolbar(ServerListPage skinnable) { - JFXCheckBox chkShowAll = new JFXCheckBox(i18n("server.manage.show_all")); - chkShowAll.selectedProperty().bindBidirectional(skinnable.showAll); - - JFXCheckBox chkShowHide = new JFXCheckBox(i18n("server.manage.show_hide")); - chkShowHide.selectedProperty().bindBidirectional(skinnable.showHide); - - return Arrays.asList( - chkShowAll, - chkShowHide, - createToolbarButton2(i18n("button.refresh"), SVG.REFRESH, skinnable::refresh), - createToolbarButton2(i18n("server.manage.add"), SVG.ADD, skinnable::addServer) - ); + private JFXCheckBox createCheckBox(Consumer consumer) { + JFXCheckBox box = new JFXCheckBox(); + // change style. + consumer.accept(box); + return box; } - @Override - protected ListCell createListCell(JFXListView listView) { - return new ServerListCell(getSkinnable()); + private void changeToolbar(HBox newToolbar) { + Node oldToolbar = toolbarPane.getCurrentNode(); + if (newToolbar != oldToolbar) { + toolbarPane.setContent(newToolbar, ContainerAnimations.FADE); + if (newToolbar == searchBar) { + Platform.runLater(searchField::requestFocus); + } + } + } + + private void search() { + isSearching.set(true); + + Bindings.unbindContent(listView.getItems(), ServerListPage.this.getItems()); + + String queryString = searchField.getText(); + if (StringUtils.isBlank(queryString)) { + listView.getItems().setAll(ServerListPage.this.getItems()); + } else { + listView.getItems().clear(); + + Predicate<@Nullable String> predicate; + try { + predicate = StringUtils.compileQuery(queryString); + } catch (Throwable e) { + LOG.warning("Illegal regular expression", e); + return; + } + + for (ServerListItem item : ServerListPage.this.getItems()) { + String serverIp = item.server.getIp(); + String serverName = item.server.getName(); + + if (predicate.test(serverIp) || predicate.test(serverName)) { + listView.getItems().add(item); + } + } + } } } } From b47379d16ef2c1d696d81b2b8d32a5b3935acd4a Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 07:35:46 +0800 Subject: [PATCH 63/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8=E5=88=97=E8=A1=A8=E9=A1=B5=E6=8C=89esc=E8=BF=94?= =?UTF-8?q?=E5=9B=9E=E4=B8=8D=E4=BA=86=E4=B8=BB=E7=95=8C=E9=9D=A2=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/server/ServerListPage.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 5583c00b1b4..da3a979cb51 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -30,6 +30,8 @@ import javafx.scene.Node; import javafx.scene.control.*; import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseButton; import javafx.scene.layout.*; import javafx.util.Duration; @@ -808,6 +810,8 @@ private final class ServerListPageSkin extends SkinBase { ComponentList.setVgrow(center, Priority.ALWAYS); center.loadingProperty().bind(ServerListPage.this.loadingProperty()); + ignoreEvent(listView, KeyEvent.KEY_PRESSED, e -> e.getCode() == KeyCode.ESCAPE); + center.setContent(listView); root.getContent().add(center); } From 693911f48f73c111098b8bfe11feeac3bb386c2b Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 09:04:18 +0800 Subject: [PATCH 64/97] =?UTF-8?q?ServerListPage=E5=A4=9A=E9=80=89=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 217 +++++++++--------- 1 file changed, 103 insertions(+), 114 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index da3a979cb51..afb3ddf95ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -32,8 +32,10 @@ import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.input.MouseButton; -import javafx.scene.layout.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.util.Duration; import javafx.util.Subscription; import org.jackhuang.hmcl.game.HMCLGameInstance; @@ -140,16 +142,23 @@ public void copyServerIp(ServerListItem item) { FXUtils.copyText(item.server.getIp(), i18n("server.manage.copy.server.ip.ok.toast")); } - public void delete(ServerListItem item) { + private void delete(ServerListItem... items) { Controllers.confirm( i18n("button.remove.confirm"), i18n("server.delete"), - () -> Task.runAsync(Schedulers.io(), item.storageEntry::delete - ).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - LOG.warning("Failed to save server data.", exception); - } else { - serverListEntries.remove(item); + () -> Task.supplyAsync(Schedulers.io(), () -> { + boolean result = false; + for (ServerListItem item : items) { + try { + item.storageEntry.delete(); + serverListEntries.remove(item); + result = true; + } catch (IOException ignored) { + } + } + return result; + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (result) { updateServerList(); } }).start(), @@ -441,120 +450,85 @@ public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newNa } } - private final class ServerListCell extends ListCell { + private final class ServerListCell extends MDListCell { private final ServerListPage page; - private final RipplerContainer graphic; - private final ImageContainer serverIcon; - private final TwoLineListItem contentLine1; - private final ServerAddressMaskPane contentLine2AddressMaskPane; - private final ServerNetworkLatencyPane serverNetworkLatencyPane; + private final ImageContainer serverIcon = new ImageContainer(32); + private final TwoLineListItem contentLine1 = new TwoLineListItem(); + private final ServerAddressMaskPane contentLine2AddressMaskPane = new ServerAddressMaskPane(""); + private final ServerNetworkLatencyPane serverNetworkLatencyPane = new ServerNetworkLatencyPane(); - private final JFXButton statusBtn; - private final Tooltip statusBtnTooltip; + private final Tooltip statusBtnTooltip = new Tooltip(); private Subscription serverStatusResultValueSubscription; private Subscription serverStatusPingingValueSubscription; - public ServerListCell(ServerListPage page) { + public ServerListCell(JFXListView listView, ServerListPage page) { + super(listView); this.page = page; - BorderPane root = new BorderPane(); - root.getStyleClass().add("md-list-cell"); - root.setPadding(new Insets(8)); - - // server icon - { - StackPane left = new StackPane(); - root.setLeft(left); - left.setPadding(new Insets(0, 8, 0, 0)); - - this.serverIcon = new ImageContainer(32); - left.getChildren().add(serverIcon); - } - - { - this.contentLine1 = new TwoLineListItem(); - this.contentLine2AddressMaskPane = new ServerAddressMaskPane(""); - contentLine2AddressMaskPane.labelAddStyleClass("subtitle"); - - HBox contentLine2 = new HBox(contentLine2AddressMaskPane); - VBox center = new VBox(contentLine1, contentLine2); - root.setCenter(center); - } + this.getStyleClass().add("server-list-cell"); - { - HBox right = new HBox(8); - root.setRight(right); - right.setAlignment(Pos.CENTER_RIGHT); - - StackPane statusPane = new StackPane(); - statusBtn = FXUtils.newToggleButton4(SVG.NONE); - statusBtn.managedProperty().bind(statusBtn.visibleProperty()); - statusBtn.setOnAction(event -> { - ServerListItem item = getItem(); - if (item != null) - page.showServerStatus(item); - }); - serverNetworkLatencyPane = new ServerNetworkLatencyPane(); - - statusPane.getChildren().add(serverNetworkLatencyPane); - statusPane.getChildren().add(statusBtn); - right.getChildren().add(statusPane); - - statusBtnTooltip = new Tooltip(); - FXUtils.installFastTooltip(statusBtn, statusBtnTooltip); - - JFXButton editBtn = FXUtils.newToggleButton4(SVG.EDIT); - right.getChildren().add(editBtn); - FXUtils.installFastTooltip(editBtn, i18n("server.manage.edit")); - editBtn.setOnAction(event -> { - ServerListItem item = getItem(); - if (item != null) - page.editServer(item); - }); + HBox container = new HBox(8); + container.setPickOnBounds(false); + container.setAlignment(Pos.CENTER_LEFT); + VBox content = new VBox(contentLine1, contentLine2AddressMaskPane); + HBox.setHgrow(content, Priority.ALWAYS); + content.setMouseTransparent(true); + setSelectable(); - JFXButton launchBtn = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); - right.getChildren().add(launchBtn); - FXUtils.installFastTooltip(launchBtn, i18n("instance.launch")); - launchBtn.setOnAction(event -> { - ServerListItem item = getItem(); - if (item != null) - page.launchAndEnterServer(item); - }); + contentLine2AddressMaskPane.labelAddStyleClass("subtitle"); - JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); - right.getChildren().add(btnMore); - btnMore.setOnAction(event -> { - ServerListItem item = getItem(); - if (item != null) - showPopupMenu(item, JFXPopup.PopupHPosition.RIGHT, 0, root.getHeight()); - }); - } + JFXButton statusBtn = FXUtils.newToggleButton4(SVG.NONE); + statusBtn.managedProperty().bind(statusBtn.visibleProperty()); + statusBtn.setOnAction(event -> { + ServerListItem item = getItem(); + if (item != null) + page.showServerStatus(item); + }); + FXUtils.installFastTooltip(statusBtn, statusBtnTooltip); - this.graphic = new RipplerContainer(root); - graphic.setOnMouseClicked(event -> { - if (event.getClickCount() != 1) - return; + JFXButton editBtn = FXUtils.newToggleButton4(SVG.EDIT); + FXUtils.installFastTooltip(editBtn, i18n("server.manage.edit")); + editBtn.setOnAction(event -> { + ServerListItem item = getItem(); + if (item != null) + page.editServer(item); + }); + JFXButton launchBtn = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); + FXUtils.installFastTooltip(launchBtn, i18n("instance.launch")); + launchBtn.setOnAction(event -> { ServerListItem item = getItem(); - if (item == null) - return; + if (item != null) { + page.launchAndEnterServer(item); + } + }); - if (event.getButton() == MouseButton.SECONDARY) - showPopupMenu(item, JFXPopup.PopupHPosition.LEFT, event.getX(), event.getY()); + JFXButton btnMore = FXUtils.newToggleButton4(SVG.MORE_VERT); + btnMore.setOnAction(event -> { + ServerListItem item = getItem(); + if (item != null) { + showPopupMenu(item, JFXPopup.PopupHPosition.RIGHT, 0, getHeight()); + } }); - } - @Override - protected void updateItem(ServerListItem item, boolean empty) { - ServerListItem oldItem = getItem(); - boolean oldEmpty = isEmpty(); + container.getChildren().setAll( + serverIcon, + content, + new StackPane(serverNetworkLatencyPane, statusBtn), + editBtn, + launchBtn, + btnMore + ); - super.updateItem(item, empty); - if (oldItem == item && oldEmpty == empty) return; + StackPane.setMargin(container, new Insets(8)); + getContainer().getChildren().setAll(container); + } + @Override + protected void updateControl(ServerListItem item, boolean empty) { if (serverStatusPingingValueSubscription != null) { serverStatusPingingValueSubscription.unsubscribe(); serverStatusPingingValueSubscription = null; @@ -564,7 +538,6 @@ protected void updateItem(ServerListItem item, boolean empty) { serverStatusResultValueSubscription = null; } - this.graphic.releaseRippleImmediately(); this.contentLine1.getTags().clear(); if (empty || item == null) { @@ -596,8 +569,6 @@ protected void updateItem(ServerListItem item, boolean empty) { .forEach(contentLine1::addTag); } - setGraphic(graphic); - item.observableServerStatus.refreshIfNoResultAsync(false); applyServerStatusResult(item, item.observableServerStatus.resultProperty().get()); @@ -658,9 +629,7 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res } } - // Popup Menu - - public void showPopupMenu(ServerListPage.ServerListItem holder, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { + private void showPopupMenu(ServerListPage.ServerListItem holder, JFXPopup.PopupHPosition hPosition, double initOffsetX, double initOffsetY) { PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); @@ -705,9 +674,9 @@ private final class ServerListPageSkin extends SkinBase { private final PauseTransition searchPause = new PauseTransition(Duration.millis(100)); private final TransitionPane toolbarPane = new TransitionPane(); - private final HBox searchBar = new HBox(); private final HBox toolbarNormal = new HBox(); + private final HBox toolbarSelecting = new HBox(); ServerListPageSkin() { super(ServerListPage.this); @@ -722,12 +691,13 @@ private final class ServerListPageSkin extends SkinBase { root.getStyleClass().add("no-padding"); listView.getStyleClass().add("no-horizontal-scrollbar"); - listView.setCellFactory(param -> new ServerListCell(ServerListPage.this)); + listView.setCellFactory(param -> new ServerListCell(listView, ServerListPage.this)); listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); { root.getContent().add(toolbarPane); FXUtils.setOverflowHidden(toolbarPane, 8); + toolbarPane.setContent(toolbarNormal, ContainerAnimations.NONE); Insets toolbarPadding = new Insets(0, 5, 0, 5); @@ -759,7 +729,6 @@ private final class ServerListPageSkin extends SkinBase { searchPause.playFromStart(); } }); - JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { changeToolbar(toolbarNormal); searchField.clear(); @@ -767,9 +736,7 @@ private final class ServerListPageSkin extends SkinBase { isSearching.set(false); Bindings.bindContent(listView.getItems(), ServerListPage.this.getItems()); }); - onEscPressed(searchField, closeSearchBar::fire); - searchBar.getChildren().setAll( createCheckBox(box -> { box.setText(i18n("server.manage.show_all")); @@ -783,7 +750,29 @@ private final class ServerListPageSkin extends SkinBase { searchField, closeSearchBar ); - toolbarPane.setContent(toolbarNormal, ContainerAnimations.NONE); + + var selectAll = createToolbarButton2(i18n("button.select_all"), SVG.SELECT_ALL, () -> listView.getSelectionModel().selectRange(0, listView.getItems().size())); + ListChangeListener listener = change -> { + selectAll.setDisable(!listView.getItems().isEmpty() + && listView.getSelectionModel().getSelectedItems().size() == listView.getItems().size()); + }; + listView.getSelectionModel().getSelectedItems().addListener(listener); + listView.getItems().addListener(listener); + toolbarSelecting.getChildren().setAll( + createToolbarButton2(i18n("button.remove"), SVG.DELETE_FOREVER, () -> { + ServerListPage.this.delete(listView.getSelectionModel().getSelectedItems().toArray(ServerListItem[]::new)); + }), + selectAll, + createToolbarButton2(i18n("button.cancel"), SVG.CANCEL, () -> + listView.getSelectionModel().clearSelection()) + ); + FXUtils.onChangeAndOperate(listView.getSelectionModel().selectedItemProperty(), selectedItem -> { + if (selectedItem == null) { + changeToolbar(isSearching.get() ? searchBar : toolbarNormal); + } else { + changeToolbar(toolbarSelecting); + } + }); } StackPane placeholderContainer = new StackPane(); From 16a6433c75a89910cb891cd2845457dc6148e873 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 09:46:07 +0800 Subject: [PATCH 65/97] =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=99=A8=E8=B5=84=E6=BA=90=E5=8C=85=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 43 +++++++++++++++---- .../ui/instances/server/ServerListPage.java | 8 +++- .../resources/assets/lang/I18N.properties | 5 ++- .../resources/assets/lang/I18N_ar.properties | 5 ++- .../resources/assets/lang/I18N_de.properties | 5 ++- .../resources/assets/lang/I18N_es.properties | 5 ++- .../resources/assets/lang/I18N_ja.properties | 5 ++- .../resources/assets/lang/I18N_lzh.properties | 5 ++- .../resources/assets/lang/I18N_ru.properties | 5 ++- .../resources/assets/lang/I18N_uk.properties | 5 ++- .../assets/lang/I18N_zh_Hans.properties | 5 ++- .../assets/lang/I18N_zh_Hant.properties | 5 ++- 12 files changed, 81 insertions(+), 20 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index f3f78aa5996..27225934f8c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.instances.server; import com.jfoenix.controls.JFXButton; +import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXDialogLayout; import com.jfoenix.controls.JFXTextField; import javafx.beans.binding.BooleanBinding; @@ -42,11 +43,11 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jetbrains.annotations.Nullable; +import java.util.Locale; import java.util.Objects; import java.util.function.Consumer; -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.ui.FXUtils.setValidateWhileTextChanged; +import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class EditServerPane extends TransitionPane implements DialogAware { @@ -57,6 +58,7 @@ public class EditServerPane extends TransitionPane implements DialogAware { private final GridPane body = new GridPane(); private final JFXTextField serverNameField = new JFXTextField(); private final JFXTextField serverIpField = new JFXTextField(); + private final JFXComboBox serverPackStatusField = new JFXComboBox<>(); private final JFXButton btnAccept = new JFXButton(); private final JFXButton btnCancel = new JFXButton(); @@ -72,15 +74,17 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference getStyleClass().add("skin-pane"); - String referenceServerName; String referenceServerIP; + Server.ServerPackStatus referenceServerPackStatus; if (reference != null) { referenceServerName = reference.getName(); referenceServerIP = reference.getIp(); + referenceServerPackStatus = reference.getServerPackStatus(); } else { referenceServerName = null; referenceServerIP = null; + referenceServerPackStatus = null; } JFXDialogLayout rootLayout = new JFXDialogLayout(); @@ -144,6 +148,26 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference } } + // serverPackStatusField + { + Label label = new Label(); + label.setText(i18n("server.resourcepack")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 2); + + serverPackStatusField.setConverter(stringConverter(status -> i18n("server.resourcepack." + status.name().toLowerCase(Locale.ROOT)))); + serverPackStatusField.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + if (newVal == null && oldVal != null) { + serverPackStatusField.setValue(oldVal); + } + }); + serverPackStatusField.getItems().setAll(Server.ServerPackStatus.values()); + body.add(serverPackStatusField, 1, 2); + serverPackStatusField.setValue( + Objects.requireNonNullElse(referenceServerPackStatus, Server.ServerPackStatus.PROMPT) + ); + } + bodyVbox.getChildren().add(body); rootLayout.setBody(bodyVbox); @@ -182,6 +206,7 @@ protected boolean computeValue() { btnAccept.setOnAction(ignored -> { String serverName; String serverIP = serverIpField.getText(); + Server.ServerPackStatus serverPackStatus = serverPackStatusField.getValue(); if (serverIP == null) return; if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { serverName = i18n("server.name.def"); @@ -190,15 +215,15 @@ protected boolean computeValue() { } if (!stillActionProgress.get()) { - onAdd(serverName, serverIP); + onAdd(serverPackStatus, serverName, serverIP); return; } fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIP, serverName)); + handleCallback.accept(reference.withIpAndName(serverIP, serverName).withPackStatus(serverPackStatus)); } else { - handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, null, serverIP, serverName)); + handleCallback.accept(new ServerListPage.IconedServer(serverPackStatus, false, null, serverIP, serverName)); } }); @@ -229,7 +254,7 @@ private void hideStillAction() { }); } - private void onAdd(String serverName, String serverIP) { + private void onAdd(Server.ServerPackStatus packStatus, String serverName, String serverIP) { body.setDisable(true); spinner.showSpinner(); @@ -251,9 +276,9 @@ private void onAdd(String serverName, String serverIP) { } else { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIP, serverName)); + handleCallback.accept(reference.withIpAndName(serverIP, serverName).withPackStatus(packStatus)); } else { - handleCallback.accept(new ServerListPage.IconedServer(Server.ServerPackStatus.PROMPT, false, status.favicon(), serverIP, serverName)); + handleCallback.accept(new ServerListPage.IconedServer(packStatus, false, status.favicon(), serverIP, serverName)); } } }).start(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index afb3ddf95ca..265703d4ea3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -118,7 +118,9 @@ public void editServer(ServerListItem item) { if (!server.equals(item.server)) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, oldValue -> oldValue.withIpAndName(server.getIp(), server.getName())); + return storage.updateEntry(item.storageEntry, oldValue -> oldValue + .withIpAndName(server.getIp(), server.getName()) + .withPackStatus(server.getServerPackStatus())); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); @@ -448,6 +450,10 @@ public IconedServer withIcon(@Nullable String newIcon) { public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { return new IconedServer(getServerPackStatus(), isHidden(), getIcon(), newIp, newName); } + + public IconedServer withPackStatus(ServerPackStatus status) { + return new IconedServer(status, isHidden(), getIcon(), getIp(), getName()); + } } private final class ServerListCell extends MDListCell { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index efd00917323..b924dde3165 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1411,12 +1411,15 @@ server.manage.status.outside.error.blocked=Server Status (Failed, Blocked) server.manage.status.outside.error.unknownhost=Server Status (Failed, Unknown host) server.manage.status.outside.pinging=Server Status (Pinging...) server.manage.status.outside.pong=Server Status (%sms) -server.motd=MOTD server.name=Server name server.name.def=Minecraft Server server.onlineplayers=Online server.playversion=Version server.playversion.unknown=Unknown +server.resourcepack=Server Resource Packs +server.resourcepack.prompt=Prompt +server.resourcepack.enabled=Enabled +server.resourcepack.disabled=Disabled server.tag.hide=Hidden server.tag.hold.current=Current server.type=Server type diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index defb6f6ac97..acc3361f4d8 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1411,12 +1411,15 @@ server.manage.status.outside.error.blocked=حالة الخادم (فشل، مح server.manage.status.outside.error.unknownhost=حالة الخادم (فشل، المضيف غير معروف) server.manage.status.outside.pinging=حالة الخادم (جارٍ الفحص...) server.manage.status.outside.pong=حالة الخادم (%sms) -server.motd=MOTD server.name=اسم الخادم server.name.def=Minecraft Server server.onlineplayers=على الانترنت server.playversion=الإصدار server.playversion.unknown=غير معروف +server.resourcepack=حزم الموارد الخاصة بالخادم +server.resourcepack.prompt=التنبيه +server.resourcepack.enabled=مفعّل +server.resourcepack.disabled=معطّل server.tag.hide=مخفي server.tag.hold.current=المحمل server.type=نوع الخادم diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index c60470e8676..caf7f4d98fe 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1410,12 +1410,15 @@ server.manage.status.outside.error.blocked=Serverstatus (Fehlgeschlagen, blockie server.manage.status.outside.error.unknownhost=Serverstatus (Fehlgeschlagen, unbekannter Host) server.manage.status.outside.pinging=Serverstatus (Pingt...) server.manage.status.outside.pong=Serverstatus (%sms) -server.motd=MOTD server.name=Name des Servers server.name.def=Minecraft Server server.onlineplayers=Online server.playversion=Version server.playversion.unknown=Unbekannt +server.resourcepack=Server-Ressourcenpakete +server.resourcepack.prompt=Nachfragen +server.resourcepack.enabled=Aktiviert +server.resourcepack.disabled=Deaktiviert server.tag.hide=Versteckt server.tag.hold.current=Aktuell server.type=Servertyp diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 241911a3fc1..ca050a98b04 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1403,12 +1403,15 @@ server.manage.status.outside.error.blocked=Serveri seisund (ebaõnnestus, blokee server.manage.status.outside.error.unknownhost=Serveri seisund (ebaõnnestus, tundmatu host) server.manage.status.outside.pinging=Serveri seisund (kontrollitakse...) server.manage.status.outside.pong=Serveri seisund (%sms) -server.motd=MOTD server.name=Serveri nimi server.name.def=Minecraft Server server.onlineplayers=Internetis server.playversion=Versión server.playversion.unknown=Desconocido +server.resourcepack=Paquetes de recursos del servidor +server.resourcepack.prompt=Preguntar +server.resourcepack.enabled=Activado +server.resourcepack.disabled=Desactivado server.tag.hide=Oculto server.tag.hold.current=PoseídoActual server.type=Tipo de servidor diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index fc863fe4f7a..ebb5d40152d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1207,12 +1207,15 @@ server.manage.status.outside.error.blocked=サーバの状態 (失敗、ブロ server.manage.status.outside.error.unknownhost=サーバの状態 (失敗、不明なホスト) server.manage.status.outside.pinging=サーバの状態 (Ping中...) server.manage.status.outside.pong=サーバの状態 (%sms) -server.motd=MOTD server.name=サーバ名 server.name.def=Minecraft Server server.onlineplayers=オンライン server.playversion=バージョン server.playversion.unknown=不明 +server.resourcepack=サーバーのリソースパック +server.resourcepack.prompt=確認 +server.resourcepack.enabled=有効 +server.resourcepack.disabled=無効 server.tag.hide=隠しの server.tag.hold.current=保有中 server.type=サーバーの種類 diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index bb556d3f164..bc87c761e7d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1219,12 +1219,15 @@ selector.custom=自定 # server.manage.status.outside.error.unknownhost= # server.manage.status.outside.pinging= # server.manage.status.outside.pong= -# server.motd= # server.name= # server.name.def= # server.onlineplayers= # server.playversion= # server.playversion.unknown= +# server.resourcepack= +# server.resourcepack.prompt= +# server.resourcepack.enabled= +# server.resourcepack.disabled= # server.tag.hide= # server.tag.hold.current= # server.type= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 5ba8344276c..1c73f1c216e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1343,12 +1343,15 @@ server.manage.status.outside.error.blocked=Статус сервера (не у server.manage.status.outside.error.unknownhost=Статус сервера (не удалось, неизвестный хост) server.manage.status.outside.pinging=Статус сервера (пинг...) server.manage.status.outside.pong=Статус сервера (%sms) -server.motd=MOTD server.name=Имя сервера server.name.def=Minecraft Server server.onlineplayers=Онлайн server.playversion=Версия server.playversion.unknown=Неизвестно +server.resourcepack=Пакеты ресурсов сервера +server.resourcepack.prompt=Подсказка +server.resourcepack.enabled=Включено +server.resourcepack.disabled=Отключено server.tag.hide=Скрытое server.tag.hold.current=ВладёныйТекущий server.type=Тип сервера diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index bff873980b3..126caa688a1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1366,12 +1366,15 @@ server.manage.status.outside.error.blocked=Статус сервера (поми server.manage.status.outside.error.unknownhost=Статус сервера (помилка, невідомий хост) server.manage.status.outside.pinging=Статус сервера (пінг...) server.manage.status.outside.pong=Статус сервера (%sms) -server.motd=MOTD server.name=Назва сервера server.name.def=Minecraft Server server.onlineplayers=Онлайн server.playversion=Версія server.playversion.unknown=Невідомо +server.resourcepack=Пакети ресурсів сервера +server.resourcepack.prompt=Підказка +server.resourcepack.enabled=Увімкнено +server.resourcepack.disabled=Вимкнено server.tag.hide=Приховане server.tag.hold.current=ВласнийПоточний server.type=Тип сервера 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 2c23b13aa33..c2c2e5315c6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1223,12 +1223,15 @@ server.manage.status.outside.error.blocked=服务器状态 (失败,被禁止) server.manage.status.outside.error.unknownhost=服务器状态 (失败,未知的主机) server.manage.status.outside.pinging=服务器状态 (检测中...) server.manage.status.outside.pong=服务器状态 (%sms) -server.motd=MOTD server.name=服务器名字 server.name.def=Minecraft 服务器 server.onlineplayers=在线人数 server.playversion=版本 server.playversion.unknown=未知 +server.resourcepack=服务器资源包 +server.resourcepack.prompt=询问 +server.resourcepack.enabled=启用 +server.resourcepack.disabled=禁用 server.tag.hide=隐藏 server.tag.hold.current=当前实例 server.type=服务器类型 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 b83f422d2b6..3887e812282 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1217,12 +1217,15 @@ server.manage.status.outside.error.blocked=伺服器狀態 (失敗,已封鎖) server.manage.status.outside.error.unknownhost=伺服器狀態 (失敗,未知主機) server.manage.status.outside.pinging=伺服器狀態 (Pinging...) server.manage.status.outside.pong=伺服器狀態 (%sms) -server.motd=MOTD server.name=伺服器名稱 server.name.def=Minecraft 伺服器 server.onlineplayers=線上人數 server.playversion=版本 server.playversion.unknown=未知 +server.resourcepack=伺服器資源包 +server.resourcepack.prompt=詢問 +server.resourcepack.enabled=啟用 +server.resourcepack.disabled=停用 server.tag.hide=隱藏 server.tag.hold.current=目前實例 server.type=伺服器類型 From dab17b39ac4c5722b1452765cddb1f137722cd34 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 10:26:48 +0800 Subject: [PATCH 66/97] =?UTF-8?q?=E7=BB=86=E8=8A=82=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 18 +-- .../server/ServerNetworkLatencyPane.java | 117 +++++++----------- .../server/pinger/ServerStatusPinger.java | 43 ++++--- 3 files changed, 82 insertions(+), 96 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 265703d4ea3..6be806004fe 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -149,20 +149,19 @@ private void delete(ServerListItem... items) { i18n("button.remove.confirm"), i18n("server.delete"), () -> Task.supplyAsync(Schedulers.io(), () -> { - boolean result = false; + List result = new ArrayList<>(); for (ServerListItem item : items) { try { item.storageEntry.delete(); - serverListEntries.remove(item); - result = true; + result.add(item); } catch (IOException ignored) { } } return result; }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (result) { - updateServerList(); - } + if(result.isEmpty()) return; + serverListEntries.removeAll(result); + updateServerList(); }).start(), null ); @@ -616,7 +615,12 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res // save latest icon Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, server -> server.withIcon(serverStatus.favicon())); + return storage.updateEntry(item.storageEntry, server -> { + if (Objects.equals(server.getIp(), item.server.getIp())) { + return server.withIcon(serverStatus.favicon()); + } + return server; + }); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java index 3e972a6125c..37211a679da 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java @@ -20,99 +20,76 @@ import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; -import javafx.scene.Node; import javafx.scene.layout.StackPane; import javafx.scene.shape.SVGPath; import javafx.util.Duration; import org.jackhuang.hmcl.ui.SVG; -import java.util.*; - public class ServerNetworkLatencyPane extends StackPane { - private static final Set INSTANCES = Collections.newSetFromMap(new WeakHashMap<>()); - private static final Timeline TIMELINE = new Timeline(); - - static { - TIMELINE.getKeyFrames().add(new KeyFrame(Duration.millis(125), e -> { - for (ServerNetworkLatencyPane c : List.copyOf(INSTANCES)) { - if (c.state == State.PINGING) { - c.next(); - } - } - })); - TIMELINE.setCycleCount(Animation.INDEFINITE); - TIMELINE.play(); - } + private static final SVG[] FRAMES = { + SVG.SERVER_SIGNAL_0_BAR, + SVG.SERVER_SIGNAL_1_BAR, + SVG.SERVER_SIGNAL_2_BAR, + SVG.SERVER_SIGNAL_3_BAR, + SVG.SERVER_SIGNAL_FULL, + SVG.SERVER_SIGNAL_3_BAR, + SVG.SERVER_SIGNAL_2_BAR, + SVG.SERVER_SIGNAL_1_BAR + }; - private final List pingAnimationItems; + private final SVGPath icon = SVG.NONE.createIcon(); + private final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(125), e -> nextFrame())); private int index = 0; private State state = State.PINGING; public ServerNetworkLatencyPane() { - INSTANCES.add(this); - this.pingAnimationItems = Arrays.asList( - SVG.SERVER_SIGNAL_0_BAR.createIcon(), - SVG.SERVER_SIGNAL_1_BAR.createIcon(), - SVG.SERVER_SIGNAL_2_BAR.createIcon(), - SVG.SERVER_SIGNAL_3_BAR.createIcon(), - SVG.SERVER_SIGNAL_FULL.createIcon(), - SVG.SERVER_SIGNAL_3_BAR.createIcon(), - SVG.SERVER_SIGNAL_2_BAR.createIcon(), - SVG.SERVER_SIGNAL_1_BAR.createIcon() - ); - this.pingAnimationItems.forEach(c -> { - c.setOpacity(0.3); - c.setManaged(false); - c.setVisible(false); - }); + timeline.setCycleCount(Animation.INDEFINITE); + getChildren().setAll(icon); + sceneProperty().addListener((obs, o, n) -> updateTimeline()); + ping(); } - private void next() { - Node currentNode = pingAnimationItems.get(index++ % pingAnimationItems.size()); - Node nextNode = pingAnimationItems.get(index % pingAnimationItems.size()); - - currentNode.setVisible(false); - currentNode.setManaged(false); - nextNode.setVisible(true); - nextNode.setManaged(true); + private void nextFrame() { + setIcon(FRAMES[index++ % FRAMES.length], "", 0.3); } - public void error() { - state = State.ERROR; - getChildren().clear(); - getChildren().add(createIcon(SVG.SERVER_SIGNAL_ERROR, "-fx-fill: -monet-error;")); + private void updateTimeline() { + if (state == State.PINGING && getScene() != null) { + timeline.play(); + } else { + timeline.stop(); + } } - private SVGPath createIcon(SVG svg, String style) { - SVGPath icon = createIcon(svg); - icon.setStyle(style); - return icon; + public void ping() { + state = State.PINGING; + index = 0; + nextFrame(); + updateTimeline(); } - private SVGPath createIcon(SVG svg) { - return svg.createIcon(); + public void pong(long latency) { + state = State.PONG; + updateTimeline(); + + SVG svg = latency < 150 ? SVG.SERVER_SIGNAL_FULL + : latency < 300 ? SVG.SERVER_SIGNAL_3_BAR + : latency < 600 ? SVG.SERVER_SIGNAL_2_BAR + : latency < 1000 ? SVG.SERVER_SIGNAL_1_BAR + : SVG.SERVER_SIGNAL_0_BAR; + setIcon(svg, "", 1); } - public void pong(long networkLatency) { - state = State.PONG; - getChildren().clear(); - if (networkLatency < 150) { - getChildren().add(createIcon(SVG.SERVER_SIGNAL_FULL)); - } else if (networkLatency < 300) { - getChildren().add(createIcon(SVG.SERVER_SIGNAL_3_BAR)); - } else if (networkLatency < 600) { - getChildren().add(createIcon(SVG.SERVER_SIGNAL_2_BAR)); - } else if (networkLatency < 1000) { - getChildren().add(createIcon(SVG.SERVER_SIGNAL_1_BAR)); - } else { - getChildren().add(createIcon(SVG.SERVER_SIGNAL_0_BAR)); - } + public void error() { + state = State.ERROR; + updateTimeline(); + setIcon(SVG.SERVER_SIGNAL_ERROR, "-fx-fill: -monet-error;", 1); } - public void ping() { - state = State.PINGING; - getChildren().clear(); - getChildren().addAll(pingAnimationItems); + private void setIcon(SVG svg, String style, double opacity) { + icon.setContent(svg.getPath()); + icon.setStyle(style); + icon.setOpacity(opacity); } private enum State { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 024047109b5..314f73c23b2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -37,30 +37,35 @@ public interface ServerStatusPinger { ); static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { - ModernServerAddress address = ModernServerAddress.fromString(serverIp); - ServerAddressResolveResult resolveResult = address.resolve(); + try { + ModernServerAddress address = ModernServerAddress.fromString(serverIp); + ServerAddressResolveResult resolveResult = address.resolve(); - if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { - return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); - } + if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { + return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); + } - ServerAddressResolveResult.SuccessResult successResult = (ServerAddressResolveResult.SuccessResult) resolveResult; - List failures = new ArrayList<>(); - for (ServerStatusPinger pinger : PINGERS) { - ServerStatusResult statusResult = pinger.getStatus(successResult); - if (statusResult.isSuccess()) { - return statusResult; - } else { - failures.add((ServerStatusResult.FailureResult) statusResult); + ServerAddressResolveResult.SuccessResult successResult = (ServerAddressResolveResult.SuccessResult) resolveResult; + List failures = new ArrayList<>(); + for (ServerStatusPinger pinger : PINGERS) { + ServerStatusResult statusResult = pinger.getStatus(successResult); + if (statusResult.isSuccess()) { + return statusResult; + } else { + failures.add((ServerStatusResult.FailureResult) statusResult); + } } - } - Exception collectException = new IOException("Failed to get server status"); - for (ServerStatusResult.FailureResult failureResult : failures) { - collectException.addSuppressed(failureResult.getException()); + Exception collectException = new IOException("Failed to get server status"); + for (ServerStatusResult.FailureResult failureResult : failures) { + collectException.addSuppressed(failureResult.getException()); + } + LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); + } catch (Exception e) { + LOG.error("Failed to get the status of server " + serverIp, e); + return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); } - LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); - return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); } @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult); From d144ed9dccba5df55c27c0b0c6adbe830e0007cb Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 10:56:10 +0800 Subject: [PATCH 67/97] update ServerListPage --- .../org/jackhuang/hmcl/ui/instances/server/ServerListPage.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 6be806004fe..cf9b14adc70 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -159,7 +159,7 @@ private void delete(ServerListItem... items) { } return result; }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if(result.isEmpty()) return; + if (result.isEmpty()) return; serverListEntries.removeAll(result); updateServerList(); }).start(), From 7d57517a75627c3c43b008e9238faf8f03209b21 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 11:21:30 +0800 Subject: [PATCH 68/97] =?UTF-8?q?=E7=BB=86=E8=8A=82=E9=97=AE=E9=A2=98?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/Instances.java | 7 +++++-- .../java/org/jackhuang/hmcl/server/Server.java | 18 ++++++++++-------- .../org/jackhuang/hmcl/server/ServerDnD.java | 13 +++++++------ .../pinger/LegacyServerStatusPinger.java | 1 + .../server/resolver/ModernServerAddress.java | 2 +- .../java/org/jackhuang/hmcl/util/NBTUtils.java | 7 ------- 6 files changed, 24 insertions(+), 24 deletions(-) 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 94994f432e3..22c88b0f5fd 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 @@ -30,7 +30,10 @@ import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.download.game.GameLibrariesTask; import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.setting.*; +import org.jackhuang.hmcl.setting.Accounts; +import org.jackhuang.hmcl.setting.AuthlibInjectorServers; +import org.jackhuang.hmcl.setting.GameDirectoryManager; +import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -383,7 +386,7 @@ public static void generateLaunchScriptForQuickEnterWorld(HMCLGameInstance gameI launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName))); } - public static void launchAndEnterServer(HMCLGameInstance gameInstance, String serverIp) { + public static void launchAndEnterServer(@Nullable HMCLGameInstance gameInstance, String serverIp) { launch(gameInstance, launcherHelper -> launcherHelper.setQuickPlayOption(new QuickPlayOption.MultiPlayer(serverIp))); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 23a7666967e..5662641110b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -17,7 +17,9 @@ */ package org.jackhuang.hmcl.server; +import org.glavo.nbt.io.NBTCodec; import org.glavo.nbt.tag.*; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -27,9 +29,6 @@ import java.util.List; import java.util.Objects; -import static org.jackhuang.hmcl.util.NBTUtils.readCompressed; -import static org.jackhuang.hmcl.util.NBTUtils.writeCompressed; - public class Server { private final @NotNull ServerPackStatus serverPackStatus; private final boolean hidden; @@ -58,12 +57,13 @@ public static Server loadSingle(@NotNull CompoundTag tag) { public static List loadFromServersDat(@NotNull Path file) throws IOException { List servers = new ArrayList<>(); - CompoundTag root = readCompressed(file); + CompoundTag root = NBTCodec.of().readTag(file, TagType.COMPOUND); Tag serversTag = root.get("servers"); - if (serversTag == null) + if (serversTag == null) { throw new IOException("servers tag not found"); - else if (!(serversTag instanceof ListTag)) + } else if (!(serversTag instanceof ListTag)) { throw new IOException("servers tag is not a ListTag"); + } for (Tag serverEntryTag : ((ListTag) serversTag)) { if (serverEntryTag instanceof CompoundTag serverEntryTagCompound) { @@ -87,7 +87,9 @@ public static void saveToServersDat(@NotNull List servers, CompoundTag root = new CompoundTag(); root.addTag("servers", tag); - writeCompressed(root, file); + FileUtils.saveSafely(file, outputStream -> + NBTCodec.of().writeTag(outputStream, root) + ); } public void writeToCompoundTag(CompoundTag tag) { @@ -115,7 +117,7 @@ public int hashCode() { return Objects.hash(serverPackStatus, hidden, icon, ip, name); } - public ServerPackStatus getServerPackStatus() { + public @NotNull ServerPackStatus getServerPackStatus() { return serverPackStatus; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java index e3c1d273e43..5ad53d67a66 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerDnD.java @@ -68,14 +68,15 @@ private ServerDnD() { } public static Optional parseUrlFromDragboard(Dragboard dragboard) { - String url = dragboard.getString(); - if (url == null) return Optional.empty(); + String input = dragboard.getString(); + if (input == null) return Optional.empty(); + if (input.length() > 32768) return Optional.empty(); - int index = url.indexOf(":"); + int index = input.indexOf(":"); if (index == -1) return Optional.empty(); - if (!SCHEME.equals(url.substring(0, index))) return Optional.empty(); + if (!SCHEME.equals(input.substring(0, index))) return Optional.empty(); try { - JsonObject object = JsonParser.parseString(new String(Base64.getDecoder().decode(url.substring(index + 1)), StandardCharsets.UTF_8)).getAsJsonObject(); + JsonObject object = JsonParser.parseString(new String(Base64.getDecoder().decode(input.substring(index + 1)), StandardCharsets.UTF_8)).getAsJsonObject(); String favicon = null; if (object.has("favicon")) { favicon = object.get("favicon").getAsString(); @@ -95,7 +96,7 @@ public static Optional parseUrlFromDragboard(Dragboard dragboard) { } public static EventHandler dragOverHandler() { - return event -> parseUrlFromDragboard(event.getDragboard()).ifPresent(url -> { + return event -> parseUrlFromDragboard(event.getDragboard()).ifPresent(server -> { event.acceptTransferModes(TransferMode.COPY); event.consume(); }); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index 660ccf180b3..07cc2d54c6d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -51,6 +51,7 @@ private LegacyServerStatusPinger() { DataInputStream in = new DataInputStream(socket.getInputStream())) { long sendTime = System.currentTimeMillis(); + // only send these 3 bytes and all legacy servers(<=1.6) will respond correspondingly out.writeByte(0xfe); // packet id out.writeByte(0x01); // payload(always 1) out.writeByte(0xfa); // plugin message diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java index 844aca887ac..e7fb1b5d4bd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java @@ -70,7 +70,7 @@ public static ModernServerAddress fromString(String input) throws IOException { input = input.substring(0, queryStart); } - int atEnd = input.lastIndexOf('@'); + int atEnd = input.indexOf('@'); if (atEnd != -1) { queryProperties.put("_id", input.substring(0, atEnd)); input = input.substring(atEnd + 1); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java index 2ff76b8ea2f..7bd853f7591 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/NBTUtils.java @@ -19,7 +19,6 @@ import org.glavo.nbt.io.NBTCodec; import org.glavo.nbt.tag.*; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -35,12 +34,6 @@ public static CompoundTag readCompressed(Path file) throws IOException { return NBTCodec.of().readTag(file, TagType.COMPOUND); } - public static void writeCompressed(CompoundTag tag, Path file) throws IOException { - FileUtils.saveSafely(file, outputStream -> - NBTCodec.of().writeTag(outputStream, tag) - ); - } - public static OptionalLong tryGetLong(Tag tag) { if (tag instanceof ValueTag valueTag) { Object val = valueTag.getValue(); From f03642b0be15a49f68509f78579d4d516f32fae3 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 13:00:24 +0800 Subject: [PATCH 69/97] =?UTF-8?q?=E5=8F=82=E8=80=83=E5=8E=9F=E7=89=88?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E8=B0=83=E6=95=B4=20EditServerPane=20?= =?UTF-8?q?=E7=9A=84=20validator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinecraftServerAddressValidator.java | 49 +++++++++++ .../ui/instances/server/EditServerPane.java | 6 +- .../ui/instances/server/ServerListPage.java | 9 ++- .../resources/assets/lang/I18N.properties | 2 +- .../resources/assets/lang/I18N_ar.properties | 2 +- .../resources/assets/lang/I18N_de.properties | 2 +- .../resources/assets/lang/I18N_es.properties | 2 +- .../resources/assets/lang/I18N_ja.properties | 2 +- .../resources/assets/lang/I18N_lzh.properties | 2 +- .../resources/assets/lang/I18N_ru.properties | 2 +- .../resources/assets/lang/I18N_uk.properties | 2 +- .../assets/lang/I18N_zh_Hans.properties | 2 +- .../assets/lang/I18N_zh_Hant.properties | 2 +- .../server/pinger/ServerStatusPinger.java | 2 +- .../hmcl/server/resolver/HostAndPort.java | 54 +++++++++++++ .../server/resolver/ModernServerAddress.java | 81 ++++++++----------- 16 files changed, 158 insertions(+), 63 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java new file mode 100644 index 00000000000..c54d6e3cfa5 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java @@ -0,0 +1,49 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui.construct; + +import com.jfoenix.validation.base.ValidatorBase; +import javafx.scene.control.TextInputControl; +import org.jackhuang.hmcl.server.resolver.ModernServerAddress; + +import static org.jackhuang.hmcl.util.i18n.I18n.i18n; + +public class MinecraftServerAddressValidator extends ValidatorBase { + + public MinecraftServerAddressValidator() { + super(i18n("server.address.malformed")); + } + + @Override + protected void eval() { + if (srcControl.get() instanceof TextInputControl) { + evalTextInputField(); + } + } + + private void evalTextInputField() { + TextInputControl textField = ((TextInputControl) srcControl.get()); + + try { + ModernServerAddress.parse(textField.getText()); + hasErrors.set(false); + } catch (Exception e) { + hasErrors.set(true); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 27225934f8c..1ca47aa73e9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -39,7 +39,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.RequiredValidator; +import org.jackhuang.hmcl.ui.construct.MinecraftServerAddressValidator; import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jetbrains.annotations.Nullable; @@ -139,13 +139,15 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); - serverIpField.setValidators(new RequiredValidator()); + serverIpField.setValidators(new MinecraftServerAddressValidator()); setValidateWhileTextChanged(serverIpField, true); + body.add(serverIpField, 1, 1); if (referenceServerIP != null) { serverIpField.setText(referenceServerIP); } + } // serverPackStatusField diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index cf9b14adc70..102e7f29ee5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -125,7 +125,7 @@ public void editServer(ServerListItem item) { if (exception != null) LOG.warning("Failed to save server data.", exception); - if (updated) { + if (updated != null && updated) { int index = serverListEntries.indexOf(item); if (index != -1) { serverListEntries.set(index, new ServerListItem(item.storageEntry)); @@ -154,12 +154,13 @@ private void delete(ServerListItem... items) { try { item.storageEntry.delete(); result.add(item); - } catch (IOException ignored) { + } catch (IOException e) { + LOG.error("Failed to delete server entry.", e); } } return result; }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (result.isEmpty()) return; + if (result == null || result.isEmpty()) return; serverListEntries.removeAll(result); updateServerList(); }).start(), @@ -625,7 +626,7 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res if (exception != null) LOG.warning("Failed to save server data.", exception); - if (updated) { + if (updated != null && updated) { int index = serverListEntries.indexOf(item); if (index != -1) { serverListEntries.set(index, new ServerListItem(item.storageEntry, item.observableServerStatus)); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b924dde3165..26bd36abcd6 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1375,7 +1375,7 @@ selector.choose=Choose selector.choose_file=Choose file selector.choose_directory=Choose directory selector.custom=Custom - +server.address.malformed=Invalid server address server.and_more_mods=... and %s more mods server.and_more_players=... and %s more players server.anonymous_player=Anonymous player diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index acc3361f4d8..c3ea733ef34 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1375,7 +1375,7 @@ selector.choose=اختر selector.choose_file=اختر ملفاً # selector.choose_directory= selector.custom=مخصص - +server.address.malformed=عنوان الخادم غير صالح server.and_more_mods=... و %s إضافية server.and_more_players=... و %s لاعبًا إضافيًا server.anonymous_player=لاعب مجهول diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index caf7f4d98fe..96f1ee7ba69 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1374,7 +1374,7 @@ selector.choose=Auswählen selector.choose_file=Datei auswählen selector.choose_directory=Verzeichnis auswählen selector.custom=Benutzerdefiniert - +server.address.malformed=Ungültige Serveradresse server.and_more_mods=... und %s weitere Mods server.and_more_players=... und %s weitere Spieler server.anonymous_player=Anonymer Spieler diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index ca050a98b04..7a3892ccf8c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1367,7 +1367,7 @@ selector.choose=Elegir selector.choose_file=Elegir archivo # selector.choose_directory= selector.custom=Personalizar - +server.address.malformed=Dirección del servidor no válida server.and_more_mods=... y %s mods más server.and_more_players=... y %s jugadores más server.anonymous_player=Jugador anónimo diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index ebb5d40152d..73bacf177d2 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1171,7 +1171,7 @@ selector.choose=選択 selector.choose_file=ファイルを選択します # selector.choose_directory= selector.custom=カスタム - +server.address.malformed=サーバーアドレスが無効です server.and_more_mods=... さらに %s 個の mod server.and_more_players=... さらに %s 人 server.anonymous_player=匿名プレイヤー diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index bc87c761e7d..a48e909901b 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1183,7 +1183,7 @@ selector.choose=擇 selector.choose_file=擇案 # selector.choose_directory= selector.custom=自定 - +server.address.malformed=伺服器地址不合法 # server.and_more_mods= # server.and_more_players= # server.anonymous_player= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 1c73f1c216e..1c0e7192f6a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1307,7 +1307,7 @@ selector.choose=Выберите selector.choose_file=Выберите файл selector.choose_directory=Выберите папку selector.custom=Свой - +server.address.malformed=Неверный адрес сервера server.and_more_mods=... и ещё %s модов server.and_more_players=... и ещё %s игроков server.anonymous_player=Анонимный игрок diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 126caa688a1..f1382420e6a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1330,7 +1330,7 @@ selector.choose=Вибрати selector.choose_file=Вибрати файл selector.choose_directory=Виберіть каталог selector.custom=Власний - +server.address.malformed=Невірна адреса сервера server.and_more_mods=... і ще %s модів server.and_more_players=... і ще %s гравців server.anonymous_player=Анонімний гравець 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 c2c2e5315c6..4566c408083 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1187,7 +1187,7 @@ selector.choose=选择 selector.choose_file=选择文件 selector.choose_directory=选择文件夹 selector.custom=自定义 - +server.address.malformed=地址不合法 server.and_more_mods=... 及其他 %s 个模组 server.and_more_players=... 及其他 %s 名玩家 server.anonymous_player=匿名玩家 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 3887e812282..6b67f187a72 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1181,7 +1181,7 @@ selector.choose=選取 selector.choose_file=選取檔案 selector.choose_directory=選取目錄 selector.custom=自訂 - +server.address.malformed=伺服器位址無效 server.and_more_mods=... 及其他 %s 個模組 server.and_more_players=... 及其他 %s 名玩家 server.anonymous_player=匿名玩家 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 314f73c23b2..b5a956efa70 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -38,7 +38,7 @@ public interface ServerStatusPinger { static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { try { - ModernServerAddress address = ModernServerAddress.fromString(serverIp); + ModernServerAddress address = ModernServerAddress.parse(serverIp); ServerAddressResolveResult resolveResult = address.resolve(); if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java index 7c101504980..031b1791729 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java @@ -35,4 +35,58 @@ public record HostAndPort( public boolean equals(@NotNull String host, int port) { return host.equals(this.host) && port == this.port; } + + public static @NotNull HostAndPort parseHostAndPort(@NotNull String input, int defaultPort) { + String host; + String portString; + + if (!input.startsWith("[")) { + int colonPos = input.indexOf(':'); + if (colonPos >= 0 && input.indexOf(':', colonPos + 1) == -1) { + host = input.substring(0, colonPos); + portString = input.substring(colonPos + 1); + } else { + host = input; + portString = null; + } + } else { + int colonIndex = input.indexOf(':'); + int closeBracketIndex = input.lastIndexOf(']'); + if (!(colonIndex > -1 && closeBracketIndex > colonIndex)) { + throw new IllegalArgumentException("Invalid server address: " + input); + } + + if (closeBracketIndex + 1 == input.length()) { + host = input.substring(1, closeBracketIndex); + portString = null; + } else { + host = input.substring(1, closeBracketIndex); + if (!(input.charAt(closeBracketIndex + 1) == ':')) { + throw new IllegalArgumentException("Invalid server address: " + input); + } + for (int i = closeBracketIndex + 2; i < input.length(); ++i) { + if (!Character.isDigit(input.charAt(i))) { + throw new IllegalArgumentException("Invalid server address: " + input); + } + } + portString = input.substring(closeBracketIndex + 2); + } + } + + int port; + if (portString != null && !(portString = portString.trim()).isEmpty()) { + try { + port = Integer.parseInt(portString); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid server address: " + input, e); + } + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Port number out of range: " + input); + } + } else { + port = defaultPort; + } + + return new HostAndPort(host, port); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java index e7fb1b5d4bd..edc5fe426a0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java @@ -18,8 +18,8 @@ package org.jackhuang.hmcl.server.resolver; import org.jackhuang.hmcl.server.ServerBlockedByMojangException; +import org.jetbrains.annotations.NotNull; -import java.io.IOException; import java.net.InetSocketAddress; import java.net.URLDecoder; import java.net.UnknownHostException; @@ -42,56 +42,45 @@ private ModernServerAddress( this.queryProperties = queryProperties; } - public static ModernServerAddress fromString(String input) throws IOException { - String rawServerIp = input; - if (input == null || input.isEmpty()) throw new IOException("server ip is empty."); - - - Map queryProperties = new LinkedHashMap<>(); - - int queryStart = input.lastIndexOf('?'); - if (queryStart != -1) { - String queryPropertyStr = input.substring(queryStart + 1); - String[] args = queryPropertyStr.split("&"); - for (String arg : args) { - int separator = arg.indexOf('='); - - String key; - String value; - if (separator >= 0) { - key = URLDecoder.decode(arg.substring(0, separator), StandardCharsets.UTF_8); - value = URLDecoder.decode(arg.substring(separator + 1), StandardCharsets.UTF_8); - } else { - key = URLDecoder.decode(arg, StandardCharsets.UTF_8); - value = ""; + public static ModernServerAddress parse(@NotNull String input) { + try { + input = input.trim(); + String rawServerIp = input; + if (input.isEmpty()) throw new IllegalArgumentException("server ip is empty."); + + Map queryProperties = new LinkedHashMap<>(); + + int queryStart = input.lastIndexOf('?'); + if (queryStart != -1) { + String queryPropertyStr = input.substring(queryStart + 1); + String[] args = queryPropertyStr.split("&"); + for (String arg : args) { + int separator = arg.indexOf('='); + + String key; + String value; + if (separator >= 0) { + key = URLDecoder.decode(arg.substring(0, separator), StandardCharsets.UTF_8); + value = URLDecoder.decode(arg.substring(separator + 1), StandardCharsets.UTF_8); + } else { + key = URLDecoder.decode(arg, StandardCharsets.UTF_8); + value = ""; + } + queryProperties.put(key, value); } - queryProperties.put(key, value); + input = input.substring(0, queryStart); } - input = input.substring(0, queryStart); - } - int atEnd = input.indexOf('@'); - if (atEnd != -1) { - queryProperties.put("_id", input.substring(0, atEnd)); - input = input.substring(atEnd + 1); - } - - String host = input; - int port = 25565; - - int lastIndexOf = input.lastIndexOf(":"); - if (lastIndexOf != -1) { - String portArea = input.substring(lastIndexOf + 1); - try { - int parsedPort = Integer.parseInt(portArea); - if (parsedPort > 0 && parsedPort <= 65535) { - host = input.substring(0, lastIndexOf); - port = parsedPort; - } - } catch (Exception ignore) { + int atEnd = input.indexOf('@'); + if (atEnd != -1) { + queryProperties.put("_id", input.substring(0, atEnd)); + input = input.substring(atEnd + 1); } + + return new ModernServerAddress(rawServerIp, HostAndPort.parseHostAndPort(input, 25565), queryProperties); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse URL " + input, e); } - return new ModernServerAddress(rawServerIp, new HostAndPort(host, port), queryProperties); } public ServerAddressResolveResult resolve() { From cb6ac86dc3efc2c4a1495014631df5d9d7534478 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 13:24:40 +0800 Subject: [PATCH 70/97] =?UTF-8?q?=E4=B9=9F=E4=B8=8D=E5=85=81=E8=AE=B8?= =?UTF-8?q?=E7=A9=BAhost?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/EditServerPane.java | 10 +++++----- .../jackhuang/hmcl/server/resolver/HostAndPort.java | 5 ++++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 1ca47aa73e9..c908561db48 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -37,10 +37,7 @@ import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.DialogAware; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.MinecraftServerAddressValidator; -import org.jackhuang.hmcl.ui.construct.SpinnerPane; +import org.jackhuang.hmcl.ui.construct.*; import org.jetbrains.annotations.Nullable; import java.util.Locale; @@ -139,7 +136,10 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference GridPane.setHalignment(label, HPos.LEFT); body.add(label, 0, 1); - serverIpField.setValidators(new MinecraftServerAddressValidator()); + serverIpField.setValidators( + new RequiredValidator(), + new MinecraftServerAddressValidator() + ); setValidateWhileTextChanged(serverIpField, true); body.add(serverIpField, 1, 1); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java index 031b1791729..7ec4844a5fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java @@ -81,11 +81,14 @@ public boolean equals(@NotNull String host, int port) { throw new IllegalArgumentException("Invalid server address: " + input, e); } if (port < 0 || port > 65535) { - throw new IllegalArgumentException("Port number out of range: " + input); + throw new IllegalArgumentException("Invalid server address: " + input); } } else { port = defaultPort; } + if (host.trim().isEmpty()) { + throw new IllegalArgumentException("Invalid server address: " + input); + } return new HostAndPort(host, port); } From 25b757498587967136cf5cd62815033067504f12 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 21:10:15 +0800 Subject: [PATCH 71/97] =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AF=B9=2026.4-snapshot-1=20=E5=B8=A6?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=9A=84=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=9C=B0?= =?UTF-8?q?=E5=9D=80=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinecraftServerAddressValidator.java | 4 +- .../java/org/jackhuang/hmcl/game/World.java | 4 + .../hmcl/launch/DefaultLauncher.java | 17 +- .../server/pinger/ServerStatusPinger.java | 6 +- .../server/resolver/ModernServerAddress.java | 116 ------------- .../resolver/MojangBlockServerChecker.java | 2 +- .../resolver/ServerAddressResolveResult.java | 22 +-- .../resolver/ServerDnsSrvRedirector.java | 10 +- .../resolver => util}/HostAndPort.java | 38 +++-- .../jackhuang/hmcl/util/ServerAddress.java | 156 +++++++++++------- .../jackhuang/hmcl/util/HostAndPortTest.java | 62 +++++++ .../hmcl/util/ServerAddressTest.java | 57 +++---- 12 files changed, 256 insertions(+), 238 deletions(-) delete mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/{server/resolver => util}/HostAndPort.java (84%) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/HostAndPortTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java index c54d6e3cfa5..2e836643f18 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java @@ -19,7 +19,7 @@ import com.jfoenix.validation.base.ValidatorBase; import javafx.scene.control.TextInputControl; -import org.jackhuang.hmcl.server.resolver.ModernServerAddress; +import org.jackhuang.hmcl.util.ServerAddress; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -40,7 +40,7 @@ private void evalTextInputField() { TextInputControl textField = ((TextInputControl) srcControl.get()); try { - ModernServerAddress.parse(textField.getText()); + ServerAddress.parse(textField.getText()); hasErrors.set(false); } catch (Exception e) { hasErrors.set(true); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 410f0082cfd..57abf7dbe63 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -236,6 +236,10 @@ public static boolean supportQuickPlay(GameVersionNumber gameVersionNumber) { return gameVersionNumber != null && gameVersionNumber.isAtLeast("1.20", "23w14a"); } + public static boolean supportQueryArgServerAddress(GameVersionNumber gameVersionNumber) { + return gameVersionNumber != null && gameVersionNumber.isAtLeast("26.4", "26.4-snapshot-1"); + } + private void loadAndCheckWorldData() throws IOException { loadAndCheckLevelData(levelDataPath); loadOtherData(); 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 86d358044a2..38627c8607f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -374,12 +374,23 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { ServerAddress parsed = ServerAddress.parse(address); if (World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion))) { res.add("--quickPlayMultiplayer"); - res.add(parsed.port() >= 0 ? address : parsed.host() + ":25565"); + if (World.supportQueryArgServerAddress(GameVersionNumber.asGameVersion(gameVersion))) { + res.add(parsed.toServerIp(true)); + } else { + res.add(parsed.toServerIp(false)); + if (!parsed.queryProperties().isEmpty()) { + LOG.warning("Ignore the query parameters in the server address."); + } + } } else { res.add("--server"); - res.add(parsed.host()); + res.add(parsed.hostAndPort().host()); res.add("--port"); - res.add(parsed.port() >= 0 ? String.valueOf(parsed.port()) : "25565"); + res.add(String.valueOf(parsed.hostAndPort().port())); + + if (!parsed.queryProperties().isEmpty()) { + LOG.warning("Ignore the query parameters in the server address."); + } } } catch (IllegalArgumentException e) { LOG.warning("Invalid server address: " + address, e); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index b5a956efa70..426cb8178e9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -18,8 +18,8 @@ package org.jackhuang.hmcl.server.pinger; import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.server.resolver.ModernServerAddress; import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; +import org.jackhuang.hmcl.util.ServerAddress; import org.jetbrains.annotations.NotNull; import java.io.DataInputStream; @@ -38,7 +38,7 @@ public interface ServerStatusPinger { static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { try { - ModernServerAddress address = ModernServerAddress.parse(serverIp); + ServerAddress address = ServerAddress.parse(serverIp); ServerAddressResolveResult resolveResult = address.resolve(); if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { @@ -60,7 +60,7 @@ public interface ServerStatusPinger { for (ServerStatusResult.FailureResult failureResult : failures) { collectException.addSuppressed(failureResult.getException()); } - LOG.error("Failed to get the status of server " + resolveResult.getRawAddress().getRawServerIp(), collectException); + LOG.error("Failed to get the status of server " + serverIp, collectException); return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, collectException); } catch (Exception e) { LOG.error("Failed to get the status of server " + serverIp, e); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java deleted file mode 100644 index edc5fe426a0..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ModernServerAddress.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.server.resolver; - -import org.jackhuang.hmcl.server.ServerBlockedByMojangException; -import org.jetbrains.annotations.NotNull; - -import java.net.InetSocketAddress; -import java.net.URLDecoder; -import java.net.UnknownHostException; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashMap; -import java.util.Map; - -public final class ModernServerAddress { - private final String rawServerIp; - private final HostAndPort hostAndPort; - private final Map queryProperties; - - private ModernServerAddress( - String rawServerIp, - HostAndPort hostAndPort, - Map queryProperties - ) { - this.rawServerIp = rawServerIp; - this.hostAndPort = hostAndPort; - this.queryProperties = queryProperties; - } - - public static ModernServerAddress parse(@NotNull String input) { - try { - input = input.trim(); - String rawServerIp = input; - if (input.isEmpty()) throw new IllegalArgumentException("server ip is empty."); - - Map queryProperties = new LinkedHashMap<>(); - - int queryStart = input.lastIndexOf('?'); - if (queryStart != -1) { - String queryPropertyStr = input.substring(queryStart + 1); - String[] args = queryPropertyStr.split("&"); - for (String arg : args) { - int separator = arg.indexOf('='); - - String key; - String value; - if (separator >= 0) { - key = URLDecoder.decode(arg.substring(0, separator), StandardCharsets.UTF_8); - value = URLDecoder.decode(arg.substring(separator + 1), StandardCharsets.UTF_8); - } else { - key = URLDecoder.decode(arg, StandardCharsets.UTF_8); - value = ""; - } - queryProperties.put(key, value); - } - input = input.substring(0, queryStart); - } - - int atEnd = input.indexOf('@'); - if (atEnd != -1) { - queryProperties.put("_id", input.substring(0, atEnd)); - input = input.substring(atEnd + 1); - } - - return new ModernServerAddress(rawServerIp, HostAndPort.parseHostAndPort(input, 25565), queryProperties); - } catch (Exception e) { - throw new IllegalArgumentException("Failed to parse URL " + input, e); - } - } - - public ServerAddressResolveResult resolve() { - try { - InetSocketAddress inetSocketAddress = hostAndPort.toInetSocketAddress(); - if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { - return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); - } - HostAndPort lookupDnsResult = ServerDnsSrvRedirector.lookup(this); - if (lookupDnsResult != null) { - inetSocketAddress = lookupDnsResult.toInetSocketAddress(); - if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { - return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); - } - } - return ServerAddressResolveResult.succeed(this, inetSocketAddress); - } catch (UnknownHostException e) { - return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST, e); - } - } - - public Map getQueryProperties() { - return queryProperties; - } - - public HostAndPort getHostAndPort() { - return hostAndPort; - } - - public String getRawServerIp() { - return rawServerIp; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java index 9ed8f0b37f3..ef9ba81185e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java @@ -40,7 +40,7 @@ private MojangBlockServerChecker() { } - static boolean isBlocking(InetSocketAddress inetSocketAddress0) { + public static boolean isBlocking(InetSocketAddress inetSocketAddress0) { if (predicate == null) { synchronized (MojangBlockServerChecker.class) { if (predicate == null) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java index 84c9a4067ad..39d8a09db9f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.server.resolver; import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.util.ServerAddress; import org.jetbrains.annotations.NotNull; import java.net.InetSocketAddress; @@ -27,22 +28,22 @@ import java.util.Map; public sealed class ServerAddressResolveResult { - private final ModernServerAddress rawAddress; + private final ServerAddress rawAddress; - private ServerAddressResolveResult(ModernServerAddress rawAddress) { + private ServerAddressResolveResult(ServerAddress rawAddress) { this.rawAddress = rawAddress; } - public static @NotNull ServerAddressResolveResult.FailureResult failed(ModernServerAddress rawAddress, FailureResult.Reason reason, Exception exception) { + public static @NotNull ServerAddressResolveResult.FailureResult failed(ServerAddress rawAddress, FailureResult.Reason reason, Exception exception) { return new FailureResult(rawAddress, reason, exception); } - public static SuccessResult succeed(ModernServerAddress rawAddress, InetSocketAddress resolvedAddress) { - Map queryProperties = new LinkedHashMap<>(rawAddress.getQueryProperties()); - boolean isSame = rawAddress.getHostAndPort().equals(resolvedAddress.getHostName(), resolvedAddress.getPort()); + public static SuccessResult succeed(ServerAddress rawAddress, InetSocketAddress resolvedAddress) { + Map queryProperties = new LinkedHashMap<>(rawAddress.queryProperties()); + boolean isSame = rawAddress.hostAndPort().equals(resolvedAddress.getHostName(), resolvedAddress.getPort()); if (!isSame || queryProperties.get("_o") != null) { - queryProperties.put("_o", rawAddress.getHostAndPort().host() + ":" + rawAddress.getHostAndPort().port()); + queryProperties.put("_o", rawAddress.hostAndPort().host() + ":" + rawAddress.hostAndPort().port()); } StringBuilder queryArgBuilder = new StringBuilder(); @@ -60,6 +61,7 @@ public static SuccessResult succeed(ModernServerAddress rawAddress, InetSocketAd queryArgBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); } + // include port if (queryArgBuilder.isEmpty()) { queryArgBuilder.insert(0, resolvedAddress.getHostName()); } else { @@ -68,7 +70,7 @@ public static SuccessResult succeed(ModernServerAddress rawAddress, InetSocketAd return new SuccessResult(rawAddress, resolvedAddress, queryArgBuilder.toString(), resolvedAddress.getPort()); } - public ModernServerAddress getRawAddress() { + public ServerAddress getRawAddress() { return rawAddress; } @@ -76,7 +78,7 @@ public static final class FailureResult extends ServerAddressResolveResult { private final Reason reason; private final Exception exception; - public FailureResult(ModernServerAddress rawAddress, Reason reason, Exception exception) { + public FailureResult(ServerAddress rawAddress, Reason reason, Exception exception) { super(rawAddress); this.reason = reason; this.exception = exception; @@ -108,7 +110,7 @@ public static final class SuccessResult extends ServerAddressResolveResult { private final String packerHandshakeAddress; private final int packerHandshakePort; - public SuccessResult(ModernServerAddress rawAddress, InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { + public SuccessResult(ServerAddress rawAddress, InetSocketAddress connectAddress, String packerHandshakeAddress, int packerHandshakePort) { super(rawAddress); this.connectAddress = connectAddress; this.packerHandshakeAddress = packerHandshakeAddress; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java index 3c840d09a72..c8296075ecd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerDnsSrvRedirector.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.server.resolver; +import org.jackhuang.hmcl.util.HostAndPort; +import org.jackhuang.hmcl.util.ServerAddress; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -35,7 +37,7 @@ private ServerDnsSrvRedirector() { } - static @Nullable HostAndPort lookup(ModernServerAddress address0) { + public static @Nullable HostAndPort lookup(ServerAddress address0) { if (redirector == null) { synchronized (ServerDnsSrvRedirector.class) { if (redirector == null) { @@ -49,9 +51,9 @@ private ServerDnsSrvRedirector() { DirContext context = new InitialDirContext(env); redirector = (address) -> { - if (address.getHostAndPort().port() == 25565) { + if (address.hostAndPort().port() == 25565) { try { - Attributes attributes = context.getAttributes("_minecraft._tcp." + address.getHostAndPort().host(), new String[]{"SRV"}); + Attributes attributes = context.getAttributes("_minecraft._tcp." + address.hostAndPort().host(), new String[]{"SRV"}); Attribute srvAttribute = attributes.get("srv"); if (srvAttribute != null) { String[] arguments = srvAttribute.get().toString().split(" ", 4); @@ -81,6 +83,6 @@ private ServerDnsSrvRedirector() { @FunctionalInterface private interface DnsSrvRedirector { - @Nullable HostAndPort lookup(@NotNull ModernServerAddress address); + @Nullable HostAndPort lookup(@NotNull ServerAddress address); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/HostAndPort.java similarity index 84% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/util/HostAndPort.java index 7ec4844a5fa..770b7f31551 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/HostAndPort.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/HostAndPort.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server.resolver; +package org.jackhuang.hmcl.util; import org.jetbrains.annotations.NotNull; @@ -28,12 +28,10 @@ public record HostAndPort( int port ) { - @NotNull InetSocketAddress toInetSocketAddress() throws UnknownHostException { - return new InetSocketAddress(InetAddress.getByName(host), port); - } - - public boolean equals(@NotNull String host, int port) { - return host.equals(this.host) && port == this.port; + public HostAndPort { + if (port < 0 || port > 65535) { + throw new IllegalArgumentException("Invalid server address: " + port); + } } public static @NotNull HostAndPort parseHostAndPort(@NotNull String input, int defaultPort) { @@ -80,9 +78,6 @@ public boolean equals(@NotNull String host, int port) { } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid server address: " + input, e); } - if (port < 0 || port > 65535) { - throw new IllegalArgumentException("Invalid server address: " + input); - } } else { port = defaultPort; } @@ -92,4 +87,27 @@ public boolean equals(@NotNull String host, int port) { return new HostAndPort(host, port); } + + @NotNull InetSocketAddress createInetSocketAddress() throws UnknownHostException { + return new InetSocketAddress(InetAddress.getByName(host), port); + } + + public boolean equals(@NotNull String host, int port) { + return host.equals(this.host) && port == this.port; + } + + @Override + public @NotNull String toString() { + StringBuilder builder = new StringBuilder(); + + if (host.indexOf(':') >= 0) { + builder.append('[').append(host).append(']'); + } else { + builder.append(host); + } + builder.append(':').append(port); + + return builder.toString(); + } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java index 4823a7ea2fe..6f114ad4d5c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java @@ -17,86 +17,126 @@ */ package org.jackhuang.hmcl.util; +import org.jackhuang.hmcl.server.ServerBlockedByMojangException; +import org.jackhuang.hmcl.server.resolver.MojangBlockServerChecker; +import org.jackhuang.hmcl.server.resolver.ServerAddressResolveResult; +import org.jackhuang.hmcl.server.resolver.ServerDnsSrvRedirector; import org.jetbrains.annotations.NotNull; -import java.util.Objects; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; /** * @author Glavo */ -public record ServerAddress(String host, int port) { +public record ServerAddress( + HostAndPort hostAndPort, + Map queryProperties +) { - private static final int UNKNOWN_PORT = -1; + private static final int DEFAULT_SERVER_PORT = 25565; + + public ServerAddress(@NotNull String host) { + this(host, DEFAULT_SERVER_PORT); + } private static IllegalArgumentException illegalAddress(String address) { return new IllegalArgumentException("Invalid server address: " + address); } + public ServerAddress(@NotNull String host, int port) { + this(new HostAndPort(host, port), new LinkedHashMap<>()); + } + /** - * @throws IllegalArgumentException if the address is not a valid server address + * @throws IllegalArgumentException if the input is not a valid server address */ - public static @NotNull ServerAddress parse(@NotNull String address) { - Objects.requireNonNull(address); - - if (!address.startsWith("[")) { - int colonPos = address.indexOf(':'); - if (colonPos >= 0) { - if (colonPos == address.length() - 1) - throw illegalAddress(address); - - String host = address.substring(0, colonPos); - int port; - try { - port = Integer.parseInt(address.substring(colonPos + 1)); - } catch (NumberFormatException e) { - throw illegalAddress(address); + public static @NotNull ServerAddress parse(@NotNull final String raw) { + String input = raw; + try { + input = input.trim(); + + Map queryProperties = new LinkedHashMap<>(); + + // parse query properties + int queryStart = input.lastIndexOf('?'); + if (queryStart != -1) { + String queryPropertyStr = input.substring(queryStart + 1); + String[] args = queryPropertyStr.split("&"); + for (String arg : args) { + int separator = arg.indexOf('='); + + String key; + String value; + if (separator >= 0) { + key = URLDecoder.decode(arg.substring(0, separator), StandardCharsets.UTF_8); + value = URLDecoder.decode(arg.substring(separator + 1), StandardCharsets.UTF_8); + } else { + key = URLDecoder.decode(arg, StandardCharsets.UTF_8); + value = ""; + } + queryProperties.put(key, value); } - if (port < 0 || port > 0xFFFF) - throw illegalAddress(address); - return new ServerAddress(host, port); - } else { - return new ServerAddress(address); + input = input.substring(0, queryStart); } - } else { - // Parse IPv6 address - int colonIndex = address.indexOf(':'); - int closeBracketIndex = address.lastIndexOf(']'); - - if (colonIndex < 0 || closeBracketIndex < colonIndex) - throw illegalAddress(address); - - String host = address.substring(1, closeBracketIndex); - if (closeBracketIndex == address.length() - 1) - return new ServerAddress(host); - - if (address.length() < closeBracketIndex + 3 || address.charAt(closeBracketIndex + 1) != ':') - throw illegalAddress(address); - - int port; - try { - port = Integer.parseInt(address.substring(closeBracketIndex + 2)); - } catch (NumberFormatException e) { - throw illegalAddress(address); + int atEnd = input.indexOf('@'); + if (atEnd != -1) { + queryProperties.put("_id", input.substring(0, atEnd)); + input = input.substring(atEnd + 1); } - if (port < 0 || port > 0xFFFF) - throw illegalAddress(address); - - return new ServerAddress(host, port); + return new ServerAddress(HostAndPort.parseHostAndPort(input, 25565), queryProperties); + } catch (Exception e) { + throw new IllegalArgumentException("Failed to parse URL " + raw, e); } } - public ServerAddress(@NotNull String host) { - this(host, UNKNOWN_PORT); - } - - public ServerAddress(@NotNull String host, int port) { - this.host = Objects.requireNonNull(host); - this.port = port; + public ServerAddressResolveResult resolve() { + try { + InetSocketAddress inetSocketAddress = hostAndPort.createInetSocketAddress(); + if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); + } + HostAndPort lookupDnsResult = ServerDnsSrvRedirector.lookup(this); + if (lookupDnsResult != null) { + inetSocketAddress = lookupDnsResult.createInetSocketAddress(); + if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); + } + } + return ServerAddressResolveResult.succeed(this, inetSocketAddress); + } catch (UnknownHostException e) { + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST, e); + } } - @Override - public @NotNull String toString() { - return String.format("ServerAddress[host='%s', port=%d]", host, port); + public String toServerIp(boolean containQueryArgs) { + StringBuilder sb = new StringBuilder(hostAndPort.toString()); + if (containQueryArgs) { + if (!queryProperties.isEmpty()) { + StringBuilder queryArgBuilder = new StringBuilder(); + boolean first = true; + + for (Map.Entry entry : queryProperties.entrySet()) { + String key = entry.getKey(); + if (!first) { + queryArgBuilder.append("&"); + } + first = false; + + queryArgBuilder.append(URLEncoder.encode(key, StandardCharsets.UTF_8)); + queryArgBuilder.append("="); + queryArgBuilder.append(URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)); + } + sb.append("?"); + sb.append(queryArgBuilder); + } + } + return sb.toString(); } } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/HostAndPortTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/HostAndPortTest.java new file mode 100644 index 00000000000..86680916eb7 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/HostAndPortTest.java @@ -0,0 +1,62 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * @author Glavo + */ +public final class HostAndPortTest { + + @Test + public void testParseHostAndPort() { + assertEquals(new HostAndPort("example.com", 25565), HostAndPort.parseHostAndPort("example.com", 25565)); + assertEquals(new HostAndPort("example.com", 12345), HostAndPort.parseHostAndPort("example.com:12345", 25565)); + assertEquals(new HostAndPort("example.com", 25565), HostAndPort.parseHostAndPort("example.com:", 25565)); + assertEquals(new HostAndPort("127.0.0.1", 0), HostAndPort.parseHostAndPort("127.0.0.1:0", 25565)); + + assertEquals(new HostAndPort("::1", 25565), HostAndPort.parseHostAndPort("[::1]", 25565)); + assertEquals(new HostAndPort("::1", 12345), HostAndPort.parseHostAndPort("[::1]:12345", 25565)); + assertEquals(new HostAndPort("2001:db8::1", 0), HostAndPort.parseHostAndPort("[2001:db8::1]:0", 25565)); + + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("[", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("[]:0", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("[::1]|0", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("[::1]:a", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("[::1]:65536", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("example.com:a", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("example.com:-1", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort("example.com:65536", 25565)); + assertThrows(IllegalArgumentException.class, () -> HostAndPort.parseHostAndPort(" ", 25565)); + } + + @Test + public void testToStringAndEquals() { + HostAndPort hostAndPort = new HostAndPort("example.com", 25565); + assertEquals("example.com:25565", hostAndPort.toString()); + assertEquals(new HostAndPort("example.com", 25565), hostAndPort); + assertTrue(hostAndPort.equals("example.com", 25565)); + + HostAndPort ipv6 = new HostAndPort("::1", 12345); + assertEquals("[::1]:12345", ipv6.toString()); + assertTrue(ipv6.equals("::1", 12345)); + } +} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java index 1a036c3e3f8..1b4616c4e6e 100644 --- a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/ServerAddressTest.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2025 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -19,8 +19,9 @@ import org.junit.jupiter.api.Test; +import java.util.Map; + import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; /** * @author Glavo @@ -29,34 +30,28 @@ public final class ServerAddressTest { @Test public void testParse() { - assertEquals(new ServerAddress("example.com"), ServerAddress.parse("example.com")); - assertEquals(new ServerAddress("example.com", 25565), ServerAddress.parse("example.com:25565")); - - assertEquals(new ServerAddress("127.0.0.0"), ServerAddress.parse("127.0.0.0")); - assertEquals(new ServerAddress("127.0.0.0", 0), ServerAddress.parse("127.0.0.0:0")); - assertEquals(new ServerAddress("127.0.0.0", 12345), ServerAddress.parse("127.0.0.0:12345")); - - assertEquals(new ServerAddress("::1"), ServerAddress.parse("[::1]")); - assertEquals(new ServerAddress("::1", 0), ServerAddress.parse("[::1]:0")); - assertEquals(new ServerAddress("::1", 12345), ServerAddress.parse("[::1]:12345")); - assertEquals(new ServerAddress("2001:db8::1"), ServerAddress.parse("[2001:db8::1]")); - assertEquals(new ServerAddress("2001:db8::1", 0), ServerAddress.parse("[2001:db8::1]:0")); - assertEquals(new ServerAddress("2001:db8::1", 12345), ServerAddress.parse("[2001:db8::1]:12345")); - - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[]]")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[]:0")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]|")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]|0")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:a")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:65536")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[::1]:-1")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[ ]:-1")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("[-]:-1")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:a")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:65536")); - assertThrows(IllegalArgumentException.class, () -> ServerAddress.parse("example.com:-1")); + ServerAddress address = ServerAddress.parse("override@example.com:12345?mode=survival&name=hello+world%21&_id=testId"); + assertEquals(new ServerAddress(new HostAndPort("example.com", 12345), Map.of( + "mode", "survival", + "name", "hello world!", + "_id", "override" + )), address); + + assertEquals(new ServerAddress(new HostAndPort("example.com", 25565), Map.of("key", "value", "empty", "")), + ServerAddress.parse("example.com?key=value&empty")); + assertEquals(new ServerAddress(new HostAndPort("example.com", 25565), Map.of("_id", "testId")), + ServerAddress.parse("testId@example.com")); + assertEquals(new ServerAddress(new HostAndPort("example.com", 25565), Map.of("k", "v", "x", "1")), + ServerAddress.parse("example.com?x=1&k=v")); + } + + @Test + public void testToServerIp() { + ServerAddress address = ServerAddress.parse("serverId@example.com:12345?mode=survival&name=hello+world%21"); + assertEquals("example.com:12345?mode=survival&name=hello+world%21&_id=serverId", address.toServerIp(true)); + assertEquals("example.com:12345", address.toServerIp(false)); + + assertEquals("[::1]:25565?key=value", ServerAddress.parse("[::1]?key=value").toServerIp(true)); + assertEquals("[::1]:25565", ServerAddress.parse("[::1]").toServerIp(false)); } } From e5a5736916bc49153174814289d70a8becaebf43 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 10 Oct 2026 23:12:32 +0800 Subject: [PATCH 72/97] =?UTF-8?q?=E5=BF=AB=E9=80=9F=E6=B8=B8=E7=8E=A9?= =?UTF-8?q?=E4=B8=ADServerIPField=E5=AD=97=E6=AE=B5=E8=AE=BE=E7=BD=AEValid?= =?UTF-8?q?ator=E4=B8=BAMinecraftServerAddressValidator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinecraftServerAddressValidator.java | 18 ++++++++++++++---- .../hmcl/ui/game/GameSettingsPage.java | 12 +----------- .../ui/instances/server/EditServerPane.java | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java index 2e836643f18..cbc388c22ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MinecraftServerAddressValidator.java @@ -21,12 +21,14 @@ import javafx.scene.control.TextInputControl; import org.jackhuang.hmcl.util.ServerAddress; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import java.util.Objects; public class MinecraftServerAddressValidator extends ValidatorBase { + private final boolean ignoreEmpty; - public MinecraftServerAddressValidator() { - super(i18n("server.address.malformed")); + public MinecraftServerAddressValidator(String message, boolean ignoreEmpty) { + super(message); + this.ignoreEmpty = ignoreEmpty; } @Override @@ -38,9 +40,17 @@ protected void eval() { private void evalTextInputField() { TextInputControl textField = ((TextInputControl) srcControl.get()); + String text = textField.getText(); + + if (text == null || text.isEmpty()) { + if (ignoreEmpty) { + hasErrors.set(false); + return; + } + } try { - ServerAddress.parse(textField.getText()); + ServerAddress.parse(Objects.requireNonNull(text)); hasErrors.set(false); } catch (Exception e) { hasErrors.set(true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index b5592d70899..196ba707bfd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -54,7 +54,6 @@ import org.jackhuang.hmcl.ui.instances.GameInstancePage; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.Pair; -import org.jackhuang.hmcl.util.ServerAddress; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; @@ -527,16 +526,7 @@ public GameSettingsPage( var noneOption = new RadioChoiceList.Choice<>(i18n("settings.game.quick_play.none"), QuickPlayType.NONE); var multiplayerTextField = new JFXTextField(); - multiplayerTextField.setValidators(new Validator(str -> { - if (StringUtils.isBlank(str)) - return true; - try { - ServerAddress.parse(str); - return true; - } catch (Exception ignored) { - return false; - } - })); + multiplayerTextField.setValidators(new MinecraftServerAddressValidator(null, true)); FXUtils.setValidateWhileTextChanged(multiplayerTextField, true); @Nullable JFXButton multiplayerInheritanceButton = !isPresetSetting ? createInheritanceButton() : null; var multiplayerOption = new EditorChoice<>( diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index c908561db48..44ced846e66 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -138,7 +138,7 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference serverIpField.setValidators( new RequiredValidator(), - new MinecraftServerAddressValidator() + new MinecraftServerAddressValidator(i18n("server.address.malformed"), false) ); setValidateWhileTextChanged(serverIpField, true); From 8ba89e4b75894fd3e3adf59f924256df31d85ef3 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 19:56:06 +0800 Subject: [PATCH 73/97] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20MinecraftChatCompone?= =?UTF-8?q?ntUtilsTest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 1 + .../jackhuang/hmcl/server/ServerStatus.java | 2 + .../hmcl/server/ServerStatusResult.java | 2 +- .../pinger/LegacyServerStatusPinger.java | 6 ++ .../pinger/ModernServerStatusPinger.java | 6 ++ .../server/pinger/ServerStatusPinger.java | 4 +- .../resolver/ServerAddressResolveResult.java | 2 +- .../util/MinecraftChatComponentUtils.java | 5 +- .../util/MinecraftChatComponentUtilsTest.java | 64 +++++++++++++++++++ 9 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 HMCLCore/src/test/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtilsTest.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index a49e97cfda5..0cb74e7236b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -258,6 +258,7 @@ public Decorator(Node mainPage, Consumer stageInitializer) { root.addEventFilter(DragEvent.DRAG_DROPPED, AuthlibInjectorDnD.dragDroppedHandler( url -> Controllers.dialog(new AddAuthlibInjectorServerPane(url)))); + // Setup Minecraft server DnD root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( server -> Controllers.dialog(new ServerStatusPane(null, ServerListPage.IconedServer.pack(server), true)))); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 0e178162dcb..3027d046c1d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.server; import com.google.gson.JsonElement; +import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,6 +26,7 @@ import java.util.UUID; public record ServerStatus( + @NotNull ServerStatusPinger pinger, long networkLatency, @Nullable Version version, @NotNull Players players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 308156b65b4..748952334fa 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -19,7 +19,7 @@ import org.jetbrains.annotations.Nullable; -public sealed class ServerStatusResult { +public abstract sealed class ServerStatusResult { public static ServerStatusResult success(ServerStatus serverStatus) { return new SuccessResult(serverStatus); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index 07cc2d54c6d..ed594483e01 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -81,6 +81,11 @@ private LegacyServerStatusPinger() { } } + @Override + public @NotNull String getName() { + return "Legacy Server Status Pinger"; + } + private @NotNull ServerStatus parseResult(long networkLatency, @NotNull String input) throws IOException { ServerStatus.Version version; String motd; @@ -117,6 +122,7 @@ private LegacyServerStatusPinger() { } return new ServerStatus( + this, networkLatency, version, players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java index 27381e15ffb..500cae08355 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java @@ -71,6 +71,11 @@ private ModernServerStatusPinger() { } } + @Override + public @NotNull String getName() { + return "Modern Server Status Pinger"; + } + private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatus) { JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); ServerStatus.Version version = new ServerStatus.Version(versionJsonObject.get("name").getAsString(), versionJsonObject.get("protocol").getAsInt()); @@ -130,6 +135,7 @@ private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatu } return new ServerStatus( + this, networkLatency, version, players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 426cb8178e9..67608fb07b9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -30,7 +30,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public interface ServerStatusPinger { +public sealed interface ServerStatusPinger permits LegacyServerStatusPinger, ModernServerStatusPinger { List PINGERS = List.of( ModernServerStatusPinger.instance, LegacyServerStatusPinger.instance @@ -70,6 +70,8 @@ public interface ServerStatusPinger { @NotNull ServerStatusResult getStatus(@NotNull ServerAddressResolveResult.SuccessResult successResult); + @NotNull String getName(); + @FunctionalInterface interface DataWriter { void write(DataOutputStream out) throws IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java index 39d8a09db9f..d0be838ae7e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/ServerAddressResolveResult.java @@ -27,7 +27,7 @@ import java.util.LinkedHashMap; import java.util.Map; -public sealed class ServerAddressResolveResult { +public abstract sealed class ServerAddressResolveResult { private final ServerAddress rawAddress; private ServerAddressResolveResult(ServerAddress rawAddress) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java index 565c40354da..a91020dfe89 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtils.java @@ -127,13 +127,10 @@ private static void extractTranslate(JsonObject obj, StringBuilder sb) { } private static void extractScore(JsonObject scoreObj, StringBuilder sb) { - if (hasJsonPrimitive(scoreObj, "value")) { - extractText(scoreObj.getAsJsonPrimitive("value"), sb); - return; - } if (hasJsonPrimitive(scoreObj, "name")) { extractText(scoreObj.getAsJsonPrimitive("name"), sb); } + if (hasJsonPrimitive(scoreObj, "objective")) { sb.append(':').append(scoreObj.get("objective").getAsString()); } diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtilsTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtilsTest.java new file mode 100644 index 00000000000..8193865e107 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/util/MinecraftChatComponentUtilsTest.java @@ -0,0 +1,64 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util; + +import com.google.gson.JsonParser; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class MinecraftChatComponentUtilsTest { + + @Test + public void testPrimitiveAndExtra() { + assertEquals("", MinecraftChatComponentUtils.toPlainStringFromChatComponent(null)); + + assertEquals("Hello world", + MinecraftChatComponentUtils.toPlainStringFromChatComponent(JsonParser.parseString("\"Hello world\""))); + + assertEquals("Hello world", + MinecraftChatComponentUtils.toPlainStringFromChatComponent(JsonParser.parseString("{\"text\":\"Hello \",\"extra\":[{\"text\":\"world\"}]}"))); + + assertEquals("Hello world", + MinecraftChatComponentUtils.toPlainStringFromChatComponent(JsonParser.parseString("{\"text\":\"§aHello \",\"extra\":[{\"text\":\"§lworld\"}]}"))); + } + + @Test + public void testTranslateAndScore() { + assertEquals("Server restart in 10 seconds", + MinecraftChatComponentUtils.toPlainStringFromChatComponent( + JsonParser.parseString("{\"translate\":\"multiplayer.player.joined.renamed\",\"fallback\":\"Server restart in 10 seconds\"}"))); + + assertEquals("multiplayer.player.joined.renamedAliceBob", + MinecraftChatComponentUtils.toPlainStringFromChatComponent( + JsonParser.parseString("{\"translate\":\"multiplayer.player.joined.renamed\",\"with\":[\"Alice\",\"Bob\"]}"))); + + assertEquals("Alice:test", + MinecraftChatComponentUtils.toPlainStringFromChatComponent( + JsonParser.parseString("{\"score\":{\"name\":\"Alice\",\"objective\":\"test\"}}"))); + } + + @Test + public void testRawText() { + assertEquals("FirstSecond", + MinecraftChatComponentUtils.toPlainStringFromChatComponent(JsonParser.parseString("{\"rawtext\":[{\"text\":\"First\"},{\"text\":\"Second\"}]}"))); + + assertEquals("Selected item: @p diamond", + MinecraftChatComponentUtils.toPlainStringFromChatComponent(JsonParser.parseString("{\"text\":\"Selected item: \",\"extra\":[{\"selector\":\"@p\"},{\"text\":\" diamond\"}]}"))); + } +} From efe1c675c3d2fba393a7d7c57ae555933dbb87f0 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 21:00:32 +0800 Subject: [PATCH 74/97] =?UTF-8?q?=E6=8F=90=E5=8F=96ServerStorage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 8 +- .../ui/instances/server/ServerListPage.java | 158 ++-------------- .../org/jackhuang/hmcl/server/Server.java | 16 ++ .../jackhuang/hmcl/server/ServerStorage.java | 174 ++++++++++++++++++ 4 files changed, 212 insertions(+), 144 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 44ced846e66..d3671ffb6cf 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -223,7 +223,9 @@ protected boolean computeValue() { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIP, serverName).withPackStatus(serverPackStatus)); + handleCallback.accept(ServerListPage.IconedServer.pack(reference.withIpNameAndPackStatus( + serverIP, serverName, serverPackStatus) + )); } else { handleCallback.accept(new ServerListPage.IconedServer(serverPackStatus, false, null, serverIP, serverName)); } @@ -278,7 +280,9 @@ private void onAdd(Server.ServerPackStatus packStatus, String serverName, String } else { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpAndName(serverIP, serverName).withPackStatus(packStatus)); + handleCallback.accept(ServerListPage.IconedServer.pack(reference.withIpNameAndPackStatus( + serverIP, serverName, packStatus) + )); } else { handleCallback.accept(new ServerListPage.IconedServer(packStatus, false, status.favicon(), serverIP, serverName)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 102e7f29ee5..85ac9302e5f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -42,6 +42,7 @@ import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.ServerStorage; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; @@ -55,13 +56,10 @@ import java.io.ByteArrayInputStream; import java.io.IOException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.*; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Predicate; -import java.util.function.UnaryOperator; import static org.jackhuang.hmcl.ui.FXUtils.*; import static org.jackhuang.hmcl.ui.ToolbarListPageSkin.createToolbarButton2; @@ -182,7 +180,7 @@ public void addServer(Server server) { ServerStorage storage = serverStorageMap.get(gameInstance.getServersDatFilePath()); if (storage == null) { storage = new ServerStorage(gameInstance.getServersDatFilePath()); - storage.holdInstances.add(gameInstance); + storage.getHoldInstances().add(gameInstance); serverStorageMap.put(gameInstance.getServersDatFilePath(), storage); } storageEntry = storage.add(IconedServer.pack(server)); @@ -243,11 +241,11 @@ private void refresh() { for (HMCLGameInstance instance : gameInstance.getRepository().getSnapshot().getInstances()) { if (serverStorageMap.containsKey(instance.getServersDatFilePath())) { - serverStorageMap.get(instance.getServersDatFilePath()).holdInstances.add(instance); + serverStorageMap.get(instance.getServersDatFilePath()).getHoldInstances().add(instance); } else { try { ServerStorage storage = new ServerStorage(instance.getServersDatFilePath()); - storage.holdInstances.add(instance); + storage.getHoldInstances().add(instance); serverStorageMap.put(instance.getServersDatFilePath(), storage); } catch (IOException e) { LOG.error("Failed to load servers from dat file: " + instance.getServersDatFilePath(), e); @@ -255,7 +253,7 @@ private void refresh() { } } - return serverStorageMap.values().stream().flatMap(e -> e.storageEntries.stream()).map(ServerListItem::new).toList(); + return serverStorageMap.values().stream().flatMap(e -> e.getStorageEntries().stream()).map(ServerListItem::new).toList(); } }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (refreshCount != currentRefresh) { @@ -278,133 +276,19 @@ private void addServer() { runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); } - private static final class ServerStorage { - private final List holdInstances = new ArrayList<>(); - private final Path serverDatFilePath; - private final List storageEntries; - private boolean storageValid = true; - - private ServerStorage(Path serverDatFilePath) throws IOException { - this.serverDatFilePath = serverDatFilePath; - - synchronized (serversFileHandleLockObject) { - if (Files.exists(serverDatFilePath)) { - List servers = Server.loadFromServersDat(serverDatFilePath); - storageEntries = new CopyOnWriteArrayList<>(); - for (Server server : servers) { - ServerStorageEntry entry = new ServerStorageEntry(IconedServer.pack(server)); - storageEntries.add(entry); - } - } else { - storageEntries = new CopyOnWriteArrayList<>(); - } - } - } - - private void save() throws IOException { - synchronized (serversFileHandleLockObject) { - if (!storageValid) return; - - Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); - } - } - - private void invalid() { - storageValid = false; - } - - private void deleteEntry(ServerStorageEntry entry) throws IOException { - synchronized (serversFileHandleLockObject) { - if (!storageValid) return; - - int index = storageEntries.indexOf(entry); - if (index != -1) { - ServerStorageEntry removed = storageEntries.remove(index); - try { - save(); - } catch (IOException e) { - storageEntries.add(index, removed); - throw e; - } - } - } - } - - private boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull IconedServer> updater) throws IOException { - synchronized (serversFileHandleLockObject) { - if (!storageValid) return false; - - int index = storageEntries.indexOf(entry); - if (index == -1) return false; - - IconedServer oldValue = entry.server; - IconedServer newValue = updater.apply(oldValue); - - if (oldValue.equals(newValue)) return false; - - try { - entry.server = newValue; - save(); - } catch (IOException e) { - entry.server = oldValue; - throw e; - } - - return true; - } - } - - public @Nullable ServerStorageEntry add(IconedServer server) throws IOException { - synchronized (serversFileHandleLockObject) { - if (!storageValid) return null; - ServerStorageEntry entry = new ServerStorageEntry(IconedServer.pack(server)); - - try { - storageEntries.add(entry); - save(); - } catch (IOException e) { - storageEntries.remove(entry); - throw e; - } - - return entry; - } - } - - public class ServerStorageEntry { - private volatile IconedServer server; - - public ServerStorageEntry(IconedServer server) { - this.server = server; - } - - private void delete() throws IOException { - ServerStorage.this.deleteEntry(this); - } - - private ServerStorage rootStorage() { - return ServerStorage.this; - } - - private Path getServerDatFilePath() { - return serverDatFilePath; - } - } - } - public static final class ServerListItem { - final @NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry; + final @NotNull ServerStorage.ServerStorageEntry storageEntry; final @NotNull IconedServer server; final @NotNull ObservableServerStatus observableServerStatus; - public ServerListItem(@NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry) { - this(storageEntry, new ObservableServerStatus(storageEntry.server.getIp())); + public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry) { + this(storageEntry, new ObservableServerStatus(storageEntry.getServer().getIp())); } - public ServerListItem(@NotNull ServerListPage.ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { + public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { this.storageEntry = storageEntry; - this.server = storageEntry.server; + this.server = IconedServer.pack(storageEntry.getServer()); this.observableServerStatus = observableServerStatus; } } @@ -442,18 +326,6 @@ public static IconedServer pack(Server server) { } return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); } - - public IconedServer withIcon(@Nullable String newIcon) { - return new IconedServer(getServerPackStatus(), isHidden(), newIcon, getIp(), getName()); - } - - public IconedServer withIpAndName(@Nullable String newIp, @Nullable String newName) { - return new IconedServer(getServerPackStatus(), isHidden(), getIcon(), newIp, newName); - } - - public IconedServer withPackStatus(ServerPackStatus status) { - return new IconedServer(status, isHidden(), getIcon(), getIp(), getName()); - } } private final class ServerListCell extends MDListCell { @@ -563,14 +435,14 @@ protected void updateControl(ServerListItem item, boolean empty) { contentLine1.addTag(i18n("server.tag.hide")); } - if (item.storageEntry.rootStorage().holdInstances.contains(page.gameInstance)) { + if (item.storageEntry.rootStorage().getHoldInstances().contains(page.gameInstance)) { contentLine1.addTag(i18n("server.tag.hold.current")); - item.storageEntry.rootStorage().holdInstances.stream() + item.storageEntry.rootStorage().getHoldInstances().stream() .filter(e -> !e.equals(page.gameInstance)) .map(gameInstance -> gameInstance.getId().id()) .forEach(contentLine1::addTag); } else { - item.storageEntry.rootStorage().holdInstances.stream() + item.storageEntry.rootStorage().getHoldInstances().stream() .map(gameInstance -> gameInstance.getId().id()) .forEach(contentLine1::addTag); } @@ -644,7 +516,9 @@ private void showPopupMenu(ServerListPage.ServerListItem holder, JFXPopup.PopupH PopupMenu popupMenu = new PopupMenu(); JFXPopup popup = new JFXPopup(popupMenu); - IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("server.manage.copy.to.instance"), () -> page.copyToInstance(holder), popup); + IconedMenuItem copyToInstanceMEnuItem = new IconedMenuItem(SVG.CONTENT_COPY, i18n("server.manage.copy.to.instance"), + () -> page.copyToInstance(holder), popup + ); popupMenu.getContent().addAll( new IconedMenuItem(SVG.EDIT, i18n("server.manage.edit"), () -> page.editServer(holder), popup diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 5662641110b..bdfb0e92bf3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -153,4 +153,20 @@ public enum ServerPackStatus { DISABLED(), PROMPT(); } + + public @NotNull Server withIcon(@Nullable String icon) { + return new Server(serverPackStatus, hidden, icon, ip, name); + } + + public @NotNull Server withIpAndName(@Nullable String ip, @Nullable String name) { + return new Server(serverPackStatus, hidden, icon, ip, name); + } + + public @NotNull Server withIpNameAndPackStatus(@Nullable String ip, @Nullable String name, @NotNull ServerPackStatus serverPackStatus) { + return new Server(serverPackStatus, hidden, icon, ip, name); + } + + public @NotNull Server withPackStatus(@NotNull ServerPackStatus serverPackStatus) { + return new Server(serverPackStatus, hidden, icon, ip, name); + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java new file mode 100644 index 00000000000..d06a6634cd5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java @@ -0,0 +1,174 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server; + +import org.jackhuang.hmcl.game.GameInstance; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.UnaryOperator; + +public class ServerStorage { + private final Lock handleLock = new ReentrantLock(); + private final List holdInstances = new ArrayList<>(); + private final Path serverDatFilePath; + private final List storageEntries; + private boolean storageValid = true; + + public ServerStorage(Path serverDatFilePath) throws IOException { + this.serverDatFilePath = serverDatFilePath; + + handleLock.lock(); + try { + if (Files.exists(serverDatFilePath)) { + List servers = Server.loadFromServersDat(serverDatFilePath); + storageEntries = new CopyOnWriteArrayList<>(); + for (Server server : servers) { + ServerStorageEntry entry = new ServerStorageEntry(server); + storageEntries.add(entry); + } + } else { + storageEntries = new CopyOnWriteArrayList<>(); + } + } finally { + handleLock.unlock(); + } + } + + public List getHoldInstances() { + return holdInstances; + } + + public List getStorageEntries() { + return storageEntries; + } + + private void save() throws IOException { + handleLock.lock(); + try { + if (!storageValid) return; + + Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); + } finally { + handleLock.unlock(); + } + } + + public void invalid() { + storageValid = false; + } + + private void deleteEntry(ServerStorageEntry entry) throws IOException { + handleLock.lock(); + try { + if (!storageValid) return; + + int index = storageEntries.indexOf(entry); + if (index != -1) { + ServerStorageEntry removed = storageEntries.remove(index); + try { + save(); + } catch (IOException e) { + storageEntries.add(index, removed); + throw e; + } + } + } finally { + handleLock.unlock(); + } + } + + public boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull Server> updater) throws IOException { + handleLock.lock(); + try { + if (!storageValid) return false; + + int index = storageEntries.indexOf(entry); + if (index == -1) return false; + + Server oldValue = entry.server; + Server newValue = updater.apply(oldValue); + + if (oldValue.equals(newValue)) return false; + + try { + entry.server = newValue; + save(); + } catch (IOException e) { + entry.server = oldValue; + throw e; + } + + return true; + } finally { + handleLock.unlock(); + } + } + + public @Nullable ServerStorageEntry add(Server server) throws IOException { + handleLock.lock(); + try { + if (!storageValid) return null; + ServerStorageEntry entry = new ServerStorageEntry(server); + + try { + storageEntries.add(entry); + save(); + } catch (IOException e) { + storageEntries.remove(entry); + throw e; + } + + return entry; + } finally { + handleLock.unlock(); + } + } + + public class ServerStorageEntry { + private volatile Server server; + + public ServerStorageEntry(Server server) { + this.server = server; + } + + public Server getServer() { + return server; + } + + public void delete() throws IOException { + ServerStorage.this.deleteEntry(this); + } + + public ServerStorage rootStorage() { + return ServerStorage.this; + } + + public Path getServerDatFilePath() { + return serverDatFilePath; + } + } +} From 1d17f26d76de6ee81bb749171086948781222472 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 21:27:56 +0800 Subject: [PATCH 75/97] =?UTF-8?q?=E5=88=A0=E9=99=A4IconedServer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 3 +- .../ui/instances/server/EditServerPane.java | 24 ++-- .../ui/instances/server/ServerListPage.java | 136 ++++++++---------- .../ui/instances/server/ServerStatusPane.java | 27 ++-- .../org/jackhuang/hmcl/server/Server.java | 58 +------- 5 files changed, 91 insertions(+), 157 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index 0cb74e7236b..b6a9d01f0fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -59,7 +59,6 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.JFXDialogPane; import org.jackhuang.hmcl.ui.construct.Navigator; -import org.jackhuang.hmcl.ui.instances.server.ServerListPage; import org.jackhuang.hmcl.ui.instances.server.ServerStatusPane; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Refreshable; @@ -261,7 +260,7 @@ public Decorator(Node mainPage, Consumer stageInitializer) { // Setup Minecraft server DnD root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( - server -> Controllers.dialog(new ServerStatusPane(null, ServerListPage.IconedServer.pack(server), true)))); + server -> Controllers.dialog(new ServerStatusPane(null, server, true)))); } /// Returns the node that must be installed as the scene root. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index d3671ffb6cf..08436763803 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -48,9 +48,9 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class EditServerPane extends TransitionPane implements DialogAware { - private final Consumer handleCallback; + private final Consumer handleCallback; - private final ServerListPage.IconedServer reference; + private final Server reference; private final Type type; private final GridPane body = new GridPane(); private final JFXTextField serverNameField = new JFXTextField(); @@ -64,7 +64,7 @@ public class EditServerPane extends TransitionPane implements DialogAware { private final SimpleBooleanProperty stillActionProgress = new SimpleBooleanProperty(false); - public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference, Consumer handleCallback) { + public EditServerPane(Type type, @Nullable Server reference, Consumer handleCallback) { this.type = type; this.reference = reference; this.handleCallback = handleCallback; @@ -75,9 +75,9 @@ public EditServerPane(Type type, @Nullable ServerListPage.IconedServer reference String referenceServerIP; Server.ServerPackStatus referenceServerPackStatus; if (reference != null) { - referenceServerName = reference.getName(); - referenceServerIP = reference.getIp(); - referenceServerPackStatus = reference.getServerPackStatus(); + referenceServerName = reference.name(); + referenceServerIP = reference.ip(); + referenceServerPackStatus = reference.serverPackStatus(); } else { referenceServerName = null; referenceServerIP = null; @@ -223,11 +223,9 @@ protected boolean computeValue() { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(ServerListPage.IconedServer.pack(reference.withIpNameAndPackStatus( - serverIP, serverName, serverPackStatus) - )); + handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, serverPackStatus)); } else { - handleCallback.accept(new ServerListPage.IconedServer(serverPackStatus, false, null, serverIP, serverName)); + handleCallback.accept(new Server(serverPackStatus, false, null, serverIP, serverName)); } }); @@ -280,11 +278,9 @@ private void onAdd(Server.ServerPackStatus packStatus, String serverName, String } else { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(ServerListPage.IconedServer.pack(reference.withIpNameAndPackStatus( - serverIP, serverName, packStatus) - )); + handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, packStatus)); } else { - handleCallback.accept(new ServerListPage.IconedServer(packStatus, false, status.favicon(), serverIP, serverName)); + handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); } } }).start(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 85ac9302e5f..bed08d08850 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -80,10 +80,16 @@ public class ServerListPage extends ListPageBase private int refreshCount = 0; - public void launchAndEnterServer(ServerListItem item) { - if (gameInstance != null) { - Instances.launchAndEnterServer(gameInstance, item.server.getIp()); + public static @NotNull Image parseServerFaviconImageOrDefault(@Nullable String imageBase64String) { + if (imageBase64String != null && !imageBase64String.isEmpty()) { + try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { + // png format. + return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); + } catch (Exception e) { + LOG.warning("Failed to decode server icon", e); + } } + return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); } public ServerListPage(ObservableValue instanceContext) { @@ -104,21 +110,27 @@ protected Skin createDefaultSkin() { return new ServerListPageSkin(); } + public void launchAndEnterServer(ServerListItem item) { + if (gameInstance != null) { + Instances.launchAndEnterServer(gameInstance, item.getServer().ip()); + } + } + public void showServerStatus(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new ServerStatusPane(item.observableServerStatus, item.server))); + runInFX(() -> Controllers.dialog(new ServerStatusPane(item.observableServerStatus, item.getServer()))); } } public void editServer(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.server, (server) -> { - if (!server.equals(item.server)) { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (server) -> { + if (!server.equals(item.getServer())) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); return storage.updateEntry(item.storageEntry, oldValue -> oldValue - .withIpAndName(server.getIp(), server.getName()) - .withPackStatus(server.getServerPackStatus())); + .withIpAndName(server.ip(), server.name()) + .withPackStatus(server.serverPackStatus())); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); @@ -138,10 +150,6 @@ public void editServer(ServerListItem item) { } } - public void copyServerIp(ServerListItem item) { - FXUtils.copyText(item.server.getIp(), i18n("server.manage.copy.server.ip.ok.toast")); - } - private void delete(ServerListItem... items) { Controllers.confirm( i18n("button.remove.confirm"), @@ -166,8 +174,12 @@ private void delete(ServerListItem... items) { ); } + public void copyServerIp(ServerListItem item) { + FXUtils.copyText(item.getServer().ip(), i18n("server.manage.copy.server.ip.ok.toast")); + } + public void copyToInstance(ServerListItem item) { - addServer(item.server); + addServer(item.getServer()); } public void addServer(Server server) { @@ -183,7 +195,7 @@ public void addServer(Server server) { storage.getHoldInstances().add(gameInstance); serverStorageMap.put(gameInstance.getServersDatFilePath(), storage); } - storageEntry = storage.add(IconedServer.pack(server)); + storageEntry = storage.add(server); } return storageEntry; }).whenComplete(Schedulers.javafx(), (storageEntry, exception) -> { @@ -200,25 +212,8 @@ public void addServer(Server server) { public void generateLaunchScript(ServerListItem item) { if (gameInstance != null) { - Instances.generateLaunchScriptForQuickConnectServer(gameInstance, item.server.getIp()); - } - } - - private void updateServerList() { - if (serverListEntries == null || gameInstance == null) { - getItems().clear(); - return; - } - - var stream = serverListEntries.stream(); - if (!showAll.get()) { - stream = stream.filter(entry -> entry.storageEntry.getServerDatFilePath().equals(gameInstance.getServersDatFilePath())); - } - if (!showHide.get()) { - stream = stream.filter(entry -> !entry.server.isHidden()); + Instances.generateLaunchScriptForQuickConnectServer(gameInstance, item.getServer().ip()); } - - getItems().setAll(stream.toList()); } public void loadInstance(HMCLGameInstance.Optional instance) { @@ -276,55 +271,42 @@ private void addServer() { runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); } + private void updateServerList() { + if (serverListEntries == null || gameInstance == null) { + getItems().clear(); + return; + } + + var stream = serverListEntries.stream(); + if (!showAll.get()) { + stream = stream.filter(entry -> entry.storageEntry.getServerDatFilePath().equals(gameInstance.getServersDatFilePath())); + } + if (!showHide.get()) { + stream = stream.filter(entry -> !entry.getServer().hidden()); + } + + getItems().setAll(stream.toList()); + } + public static final class ServerListItem { - final @NotNull ServerStorage.ServerStorageEntry storageEntry; - final @NotNull IconedServer server; + private final @NotNull ServerStorage.ServerStorageEntry storageEntry; + private final Image serverFaviconImage; final @NotNull ObservableServerStatus observableServerStatus; public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry) { - this(storageEntry, new ObservableServerStatus(storageEntry.getServer().getIp())); + this(storageEntry, new ObservableServerStatus(storageEntry.getServer().ip())); } public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { this.storageEntry = storageEntry; - this.server = IconedServer.pack(storageEntry.getServer()); this.observableServerStatus = observableServerStatus; - } - } - - public static class IconedServer extends Server { - final Image iconImage; - public IconedServer(ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { - super(serverPackStatus, hidden, icon, ip, name); - - iconImage = parseImageOrDefault(icon); + this.serverFaviconImage = parseServerFaviconImageOrDefault(storageEntry.getServer().icon()); } - public static IconedServer pack(Server server) { - if (server instanceof IconedServer) { - return (IconedServer) server; - } - return new IconedServer( - server.getServerPackStatus(), - server.isHidden(), - server.getIcon(), - server.getIp(), - server.getName() - ); - } - - public static @NotNull Image parseImageOrDefault(@Nullable String imageBase64String) { - if (imageBase64String != null && !imageBase64String.isEmpty()) { - try (ByteArrayInputStream bais = new ByteArrayInputStream(Base64.getDecoder().decode(imageBase64String))) { - // png format. - return FXUtils.loadImage(bais, "icon.png", 0, 0, true, true); - } catch (Exception e) { - LOG.warning("Failed to decode server icon", e); - } - } - return FXUtils.newBuiltinImage("/assets/img/unknown_server.png"); + public Server getServer() { + return storageEntry.getServer(); } } @@ -426,12 +408,12 @@ protected void updateControl(ServerListItem item, boolean empty) { statusBtnTooltip.setText(""); contentLine2AddressMaskPane.set(""); } else { - serverIcon.setImage(item.server.iconImage); - contentLine1.setTitle(item.server.getName() != null ? parseColorEscapes(item.server.getName()) : ""); + serverIcon.setImage(parseServerFaviconImageOrDefault(item.getServer().icon())); + contentLine1.setTitle(item.getServer().name() != null ? parseColorEscapes(item.getServer().name()) : ""); - contentLine2AddressMaskPane.set(item.server.getIp()); + contentLine2AddressMaskPane.set(item.getServer().ip()); - if (item.server.isHidden()) { + if (item.getServer().hidden()) { contentLine1.addTag(i18n("server.tag.hide")); } @@ -482,14 +464,14 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res serverNetworkLatencyPane.pong(serverStatus.networkLatency()); statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); // update latest server icon - serverIcon.setImage(IconedServer.parseImageOrDefault(serverStatus.favicon())); + serverIcon.setImage(parseServerFaviconImageOrDefault(serverStatus.favicon())); - if (!Objects.equals(serverStatus.favicon(), item.server.getIcon())) { + if (!Objects.equals(serverStatus.favicon(), item.getServer().icon())) { // save latest icon Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); return storage.updateEntry(item.storageEntry, server -> { - if (Objects.equals(server.getIp(), item.server.getIp())) { + if (Objects.equals(server.ip(), item.getServer().ip())) { return server.withIcon(serverStatus.favicon()); } return server; @@ -727,8 +709,8 @@ private void search() { } for (ServerListItem item : ServerListPage.this.getItems()) { - String serverIp = item.server.getIp(); - String serverName = item.server.getName(); + String serverIp = item.getServer().ip(); + String serverName = item.getServer().name(); if (predicate.test(serverIp) || predicate.test(serverName)) { listView.getItems().add(item); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index e5f345f49c4..508fd3abce6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -31,6 +31,7 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.HMCLGameRepository; +import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -49,20 +50,20 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class ServerStatusPane extends TransitionPane implements DialogAware { - private final ServerListPage.IconedServer iconedServer; + private final Server server; private final SpinnerPane refreshSpinner = new SpinnerPane(); private final JFXDialogLayout rootLayout = new JFXDialogLayout(); private final Label lblErrorMessage = new Label(); private final ObservableServerStatus observableServerStatus; private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedServer iconedServer) { - this(reference, iconedServer, false); + public ServerStatusPane(ObservableServerStatus reference, Server server) { + this(reference, server, false); } - public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedServer iconedServer, boolean fromDnD) { - this.iconedServer = iconedServer; - this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(iconedServer.getIp())); + public ServerStatusPane(ObservableServerStatus reference, Server server, boolean fromDnD) { + this.server = server; + this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(server.ip())); getStyleClass().add("skin-pane"); getChildren().setAll(rootLayout); @@ -100,11 +101,11 @@ public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedS null); } else { if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { - listPage.addServer(iconedServer); + listPage.addServer(server); } else { Instances.modifyServerList(selectedInstance); if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { - listPage.addServer(iconedServer); + listPage.addServer(server); } } } @@ -118,7 +119,7 @@ public ServerStatusPane(ObservableServerStatus reference, ServerListPage.IconedS fireEvent(new DialogCloseEvent()); HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); HMCLGameInstance selectedInstance = repository.getSelectedInstance(); - Instances.launchAndEnterServer(selectedInstance, iconedServer.getIp()); + Instances.launchAndEnterServer(selectedInstance, server.ip()); }); actions.getChildren().add(launchBtn); @@ -181,9 +182,9 @@ public void applyResult(ServerStatusResult statusResult) { } } - Image serverIcon = iconedServer.iconImage; + Image serverIcon = ServerListPage.parseServerFaviconImageOrDefault(server.icon()); if (status != null) { - serverIcon = ServerListPage.IconedServer.parseImageOrDefault(status.favicon()); + serverIcon = ServerListPage.parseServerFaviconImageOrDefault(status.favicon()); } VBox rootContent = new VBox(); @@ -208,10 +209,10 @@ public void applyResult(ServerStatusResult statusResult) { TwoLineListItem title = new TwoLineListItem(); title.getTitleLabel().setWrapText(true); - title.setTitle(iconedServer.getName()); + title.setTitle(server.name()); titleContent.getChildren().add(title); - ServerAddressMaskPane addressMaskPane = new ServerAddressMaskPane(iconedServer.getIp()); + ServerAddressMaskPane addressMaskPane = new ServerAddressMaskPane(server.ip()); addressMaskPane.labelAddStyleClass("subtitle"); titleContent.getChildren().add(addressMaskPane); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index bdfb0e92bf3..7e864bf2205 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -27,15 +27,14 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; -import java.util.Objects; - -public class Server { - private final @NotNull ServerPackStatus serverPackStatus; - private final boolean hidden; - private final @Nullable String icon; - private final @Nullable String ip; - private final @Nullable String name; +public record Server( + @NotNull ServerPackStatus serverPackStatus, + boolean hidden, + @Nullable String icon, + @Nullable String ip, + @Nullable String name +) { public Server(@NotNull ServerPackStatus serverPackStatus, boolean hidden, @Nullable String icon, @Nullable String ip, @Nullable String name) { this.serverPackStatus = serverPackStatus; this.hidden = hidden; @@ -105,49 +104,6 @@ public void writeToCompoundTag(CompoundTag tag) { if (name != null) tag.addString("name", name); } - @Override - public boolean equals(Object o) { - if (o == null || getClass() != o.getClass()) return false; - Server server = (Server) o; - return serverPackStatus == server.serverPackStatus && hidden == server.hidden && Objects.equals(icon, server.icon) && Objects.equals(ip, server.ip) && Objects.equals(name, server.name); - } - - @Override - public int hashCode() { - return Objects.hash(serverPackStatus, hidden, icon, ip, name); - } - - public @NotNull ServerPackStatus getServerPackStatus() { - return serverPackStatus; - } - - public boolean isHidden() { - return hidden; - } - - public @Nullable String getIcon() { - return icon; - } - - public @Nullable String getIp() { - return ip; - } - - public @Nullable String getName() { - return name; - } - - @Override - public String toString() { - return "Server{" + - "acceptTextures=" + serverPackStatus + - ", hidden=" + hidden + - ", icon='" + icon + '\'' + - ", ip='" + ip + '\'' + - ", name='" + name + '\'' + - '}'; - } - public enum ServerPackStatus { ENABLED(), DISABLED(), From 878b1279621cd43db3599815a576efcac40735d9 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 22:17:15 +0800 Subject: [PATCH 76/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20supportQueryArgServe?= =?UTF-8?q?rAddress=20=E6=96=B9=E6=B3=95=E6=8A=A5=E9=94=99=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E6=B8=B8=E6=88=8F=E6=97=A0=E6=B3=95=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 57abf7dbe63..b2e733e823a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -237,7 +237,7 @@ public static boolean supportQuickPlay(GameVersionNumber gameVersionNumber) { } public static boolean supportQueryArgServerAddress(GameVersionNumber gameVersionNumber) { - return gameVersionNumber != null && gameVersionNumber.isAtLeast("26.4", "26.4-snapshot-1"); + return gameVersionNumber != null && gameVersionNumber.compareTo("26.2-snapshot-2") >= 0; } private void loadAndCheckWorldData() throws IOException { From d2fafe093af00ae9d5b35822e2082d4190bfe740 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 22:25:37 +0800 Subject: [PATCH 77/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=AF=BB=E5=86=99=E4=B8=8A=E9=9D=A2=E5=AD=98=E5=9C=A8=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerListPage.java | 15 ++++++++++----- .../org/jackhuang/hmcl/server/ServerStorage.java | 9 +++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index bed08d08850..b6c22d627ec 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -124,13 +124,18 @@ public void showServerStatus(ServerListItem item) { public void editServer(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (server) -> { - if (!server.equals(item.getServer())) { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (newServer) -> { + if (!newServer.equals(item.getServer())) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, oldValue -> oldValue - .withIpAndName(server.ip(), server.name()) - .withPackStatus(server.serverPackStatus())); + return storage.updateEntry(item.storageEntry, oldServer -> { + if (oldServer.equals(item.getServer())) { + return oldServer + .withIpAndName(newServer.ip(), newServer.name()) + .withPackStatus(newServer.serverPackStatus()); + } + return oldServer; + }); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java index d06a6634cd5..68b4c8bc91a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java @@ -36,7 +36,7 @@ public class ServerStorage { private final List holdInstances = new ArrayList<>(); private final Path serverDatFilePath; private final List storageEntries; - private boolean storageValid = true; + private volatile boolean storageValid = true; public ServerStorage(Path serverDatFilePath) throws IOException { this.serverDatFilePath = serverDatFilePath; @@ -78,7 +78,12 @@ private void save() throws IOException { } public void invalid() { - storageValid = false; + handleLock.lock(); + try { + storageValid = false; + } finally { + handleLock.unlock(); + } } private void deleteEntry(ServerStorageEntry entry) throws IOException { From 9fcfaaacb7de09af266986469c1500e9940ea138 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 22:36:32 +0800 Subject: [PATCH 78/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6?= =?UTF-8?q?=E8=AF=BB=E5=86=99=E4=B8=8A=E9=9D=A2=E5=AD=98=E5=9C=A8=E7=9A=84?= =?UTF-8?q?=E4=B8=80=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index b6c22d627ec..820c53b0547 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -124,18 +124,13 @@ public void showServerStatus(ServerListItem item) { public void editServer(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (newServer) -> { - if (!newServer.equals(item.getServer())) { + runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (server) -> { + if (!server.equals(item.getServer())) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, oldServer -> { - if (oldServer.equals(item.getServer())) { - return oldServer - .withIpAndName(newServer.ip(), newServer.name()) - .withPackStatus(newServer.serverPackStatus()); - } - return oldServer; - }); + return storage.updateEntry(item.storageEntry, oldValue -> oldValue + .withIpAndName(server.ip(), server.name()) + .withPackStatus(server.serverPackStatus())); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); @@ -413,7 +408,7 @@ protected void updateControl(ServerListItem item, boolean empty) { statusBtnTooltip.setText(""); contentLine2AddressMaskPane.set(""); } else { - serverIcon.setImage(parseServerFaviconImageOrDefault(item.getServer().icon())); + serverIcon.setImage(item.serverFaviconImage); contentLine1.setTitle(item.getServer().name() != null ? parseColorEscapes(item.getServer().name()) : ""); contentLine2AddressMaskPane.set(item.getServer().ip()); From 42f20f837fdd28c0dcf4ba2bf3c1c0a3adba7d47 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 22:44:49 +0800 Subject: [PATCH 79/97] =?UTF-8?q?=E5=B0=86=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=85=A5=E5=8F=A3=E7=A7=BB=E5=88=B0=E5=BA=95?= =?UTF-8?q?=E9=83=A8=E7=AE=A1=E7=90=86=E5=BC=B9=E5=87=BA=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E5=86=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/ui/instances/GameInstancePage.java | 5 +++-- .../hmcl/ui/instances/server/ServerStatusPane.java | 8 +++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index f42fd7c53d4..627cccb4f5c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -370,8 +370,7 @@ protected Skin(GameInstancePage control) { .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) .addNavigationDrawerTab(control.tab, control.worldListTab, i18n("world.manage"), SVG.PUBLIC) - .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL) - .addNavigationDrawerTab(control.tab, control.serverListTab, i18n("server.manage"), SVG.DRESSER); + .addNavigationDrawerTab(control.tab, control.schematicsTab, i18n("schematics.manage"), SVG.SCHEMA, SVG.SCHEMA_FILL); VBox.setVgrow(sideBar, Priority.ALWAYS); PopupMenu browseList = new PopupMenu(); @@ -405,6 +404,8 @@ protected Skin(GameInstancePage control) { // Due to the sidebar height limit, we temporarily place the entries for these tabs // in the management menu; they will be moved back to the sidebar after a future UI refactor. new IconedMenuItem(SVG.SCREENSHOT_MONITOR, i18n("screenshots.manage"), () -> control.tab.select(control.screenshotsTab), managementPopup), + new IconedMenuItem(SVG.DRESSER, i18n("server.manage"), () -> control.tab.select(control.serverListTab), managementPopup), + new MenuSeparator(), new IconedMenuItem(null, i18n("instance.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup), new IconedMenuItem(null, i18n("instance.manage.remove_assets"), control::clearAssets, managementPopup), diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 508fd3abce6..ce685d1b284 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -100,13 +100,11 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean gotoDownload, null); } else { + if (Controllers.getGameInstancePage().getScene() == null) { + Instances.modifyServerList(selectedInstance); + } if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { listPage.addServer(server); - } else { - Instances.modifyServerList(selectedInstance); - if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { - listPage.addServer(server); - } } } }); From 8fbe02e82bce648fb97d6124c2fd09ccd04cd290 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 22:50:28 +0800 Subject: [PATCH 80/97] Update ServerListPage --- .../hmcl/ui/instances/server/ServerListPage.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 820c53b0547..56fac47b759 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -118,7 +118,7 @@ public void launchAndEnterServer(ServerListItem item) { public void showServerStatus(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new ServerStatusPane(item.observableServerStatus, item.getServer()))); + runInFX(item::openServerStatusPane); } } @@ -308,6 +308,10 @@ public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry, @N public Server getServer() { return storageEntry.getServer(); } + + public void openServerStatusPane() { + Controllers.dialog(new ServerStatusPane(observableServerStatus, getServer())); + } } private final class ServerListCell extends MDListCell { @@ -596,6 +600,15 @@ private final class ServerListPageSkin extends SkinBase { searchPause.playFromStart(); } }); + + listView.setOnContextMenuRequested(event -> { + ServerListItem selectedItem = listView.getSelectionModel().getSelectedItem(); + if (selectedItem != null && listView.getSelectionModel().getSelectedItems().size() == 1) { + listView.getSelectionModel().clearSelection(); + selectedItem.openServerStatusPane(); + } + }); + JFXButton closeSearchBar = createToolbarButton2(null, SVG.CLOSE, () -> { changeToolbar(toolbarNormal); searchField.clear(); From 7ab96491b9c01416fc16c318764a69cdf939ac34 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 11 Oct 2026 23:25:34 +0800 Subject: [PATCH 81/97] =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=88=A4=E6=96=AD?= =?UTF-8?q?=E6=8A=84=E9=94=99=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java index 129237a4fa8..c46fa42bf1a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/World.java @@ -237,7 +237,7 @@ public static boolean supportQuickPlay(GameVersionNumber gameVersionNumber) { } public static boolean supportQueryArgServerAddress(GameVersionNumber gameVersionNumber) { - return gameVersionNumber != null && gameVersionNumber.compareTo("26.2-snapshot-2") >= 0; + return gameVersionNumber != null && gameVersionNumber.compareTo("26.4-snapshot-1") >= 0; } private void loadAndCheckWorldData() throws IOException { From 3f8c700991ee71c5165ec36b013b9c31b77801ba Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 17:19:53 +0800 Subject: [PATCH 82/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BD=93=E6=89=93?= =?UTF-8?q?=E5=BC=80=E7=9A=84=E6=98=AF=E5=AE=9E=E4=BE=8B=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=A1=B5=E5=86=85=E7=9A=84=E9=9D=9E=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E7=AE=A1=E7=90=86tab=E6=97=B6,=20=E6=8B=96=E6=8B=BD=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=9C=8D=E5=8A=A1=E5=99=A8=E4=BC=9A=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/GameInstancePage.java | 4 ++++ .../hmcl/ui/instances/server/ServerStatusPane.java | 10 ++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 627cccb4f5c..9a26f6feae3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -80,6 +80,10 @@ public class GameInstancePage extends DecoratorAnimatedPage implements Decorator /// Last concrete instance displayed by this page. private @Nullable GameInstanceID preferredInstanceId; + public TabHeader.Tab getServerListTab() { + return serverListTab; + } + public static class WorkingDirChangedEvent extends Event { public static final EventType EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index ce685d1b284..bfc27c18486 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -103,8 +103,14 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean if (Controllers.getGameInstancePage().getScene() == null) { Instances.modifyServerList(selectedInstance); } - if (Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage listPage) { - listPage.addServer(server); + + if (!(Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage)) { + Controllers.getGameInstancePage().showServerList(); + } + + ServerListPage serverListPage = Controllers.getGameInstancePage().getServerListTab().getNode(); + if (serverListPage != null) { + serverListPage.addServer(server); } } }); From a3040a474388fb83ad7a9e5033b410ab9b6cb835 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 17:52:20 +0800 Subject: [PATCH 83/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A6=82=E6=9E=9C?= =?UTF-8?q?=E5=9C=A8=E6=9B=B4=E6=96=B0=E5=9B=BE=E6=A0=87=E7=9A=84=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E4=B8=AD=E6=94=B9=E5=8F=98=E4=BA=86=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E5=99=A8ip=E7=9A=84=E8=AF=9D=EF=BC=8C=E6=AD=A4=E5=9B=BE?= =?UTF-8?q?=E6=A0=87=E4=BC=9A=E8=A2=AB=E5=BA=94=E7=94=A8=E5=9C=A8=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=90=8E=E7=9A=84=E6=9C=8D=E5=8A=A1=E5=99=A8=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 6 +++- .../jackhuang/hmcl/server/ServerStatus.java | 2 -- .../hmcl/server/ServerStatusResult.java | 30 +++++++++++++++---- .../pinger/LegacyServerStatusPinger.java | 3 +- .../pinger/ModernServerStatusPinger.java | 3 +- .../server/pinger/ServerStatusPinger.java | 4 +-- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 56fac47b759..8a5f297f9a1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -50,6 +50,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; +import org.jackhuang.hmcl.util.ServerAddress; import org.jackhuang.hmcl.util.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -475,7 +476,10 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); return storage.updateEntry(item.storageEntry, server -> { - if (Objects.equals(server.ip(), item.getServer().ip())) { + if (Objects.equals( + ServerAddress.parse(Objects.requireNonNull(server.ip())), + ((ServerStatusResult.SuccessResult) result).getRawServerAddress()) + ) { return server.withIcon(serverStatus.favicon()); } return server; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 3027d046c1d..0e178162dcb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.server; import com.google.gson.JsonElement; -import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -26,7 +25,6 @@ import java.util.UUID; public record ServerStatus( - @NotNull ServerStatusPinger pinger, long networkLatency, @Nullable Version version, @NotNull Players players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 748952334fa..286a1591a1b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -17,11 +17,13 @@ */ package org.jackhuang.hmcl.server; +import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; +import org.jackhuang.hmcl.util.ServerAddress; import org.jetbrains.annotations.Nullable; public abstract sealed class ServerStatusResult { - public static ServerStatusResult success(ServerStatus serverStatus) { - return new SuccessResult(serverStatus); + public static ServerStatusResult success(ServerAddress rawServerAddress, ServerStatusPinger pinger, ServerStatus status) { + return new SuccessResult(rawServerAddress, pinger, status); } public static ServerStatusResult failure(FailureResult.Reason reason, Exception exception) { @@ -30,7 +32,7 @@ public static ServerStatusResult failure(FailureResult.Reason reason, Exception public @Nullable ServerStatus getIfSucceed() { if (this instanceof SuccessResult success) { - return success.serverStatus; + return success.status; } return null; } @@ -51,10 +53,26 @@ public boolean isFailure() { } public static final class SuccessResult extends ServerStatusResult { - public final ServerStatus serverStatus; + private final ServerAddress rawServerAddress; + private final ServerStatusPinger pinger; + private final ServerStatus status; - private SuccessResult(ServerStatus serverStatus) { - this.serverStatus = serverStatus; + private SuccessResult(ServerAddress rawServerAddress, ServerStatusPinger pinger, ServerStatus status) { + this.rawServerAddress = rawServerAddress; + this.pinger = pinger; + this.status = status; + } + + public ServerAddress getRawServerAddress() { + return rawServerAddress; + } + + public ServerStatusPinger getPinger() { + return pinger; + } + + public ServerStatus getServerStatus() { + return status; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index ed594483e01..72d983388dd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -74,7 +74,7 @@ private LegacyServerStatusPinger() { String response = readString(in); long networkLatency = System.currentTimeMillis() - sendTime; - return ServerStatusResult.success(parseResult(networkLatency, response)); + return ServerStatusResult.success(successResult.getRawAddress(), this, parseResult(networkLatency, response)); } } catch (Exception e) { return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); @@ -122,7 +122,6 @@ private LegacyServerStatusPinger() { } return new ServerStatus( - this, networkLatency, version, players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java index 500cae08355..4d93c683ae9 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java @@ -64,7 +64,7 @@ private ModernServerStatusPinger() { JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); - return ServerStatusResult.success(parseServerStatus); + return ServerStatusResult.success(successResult.getRawAddress(), this, parseServerStatus); } } catch (Exception e) { return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); @@ -135,7 +135,6 @@ private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatu } return new ServerStatus( - this, networkLatency, version, players, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java index 67608fb07b9..4a0d03c2a52 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ServerStatusPinger.java @@ -38,8 +38,8 @@ public sealed interface ServerStatusPinger permits LegacyServerStatusPinger, Mod static @NotNull ServerStatusResult getStatus(@NotNull String serverIp) throws IOException { try { - ServerAddress address = ServerAddress.parse(serverIp); - ServerAddressResolveResult resolveResult = address.resolve(); + ServerAddress rawAddress = ServerAddress.parse(serverIp); + ServerAddressResolveResult resolveResult = rawAddress.resolve(); if (resolveResult instanceof ServerAddressResolveResult.FailureResult failureResult) { return ServerStatusResult.failure(failureResult.getReason().getStatusReason(), failureResult.getException()); From 178f95f80798a34195ad54dc07d5bee9db0aa9b0 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 21:45:28 +0800 Subject: [PATCH 84/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8DEditPane=E5=9C=A8?= =?UTF-8?q?=E7=AD=89=E5=BE=85=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=97=B6=E6=8F=90=E5=89=8D=E5=8F=96=E6=B6=88=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E5=85=B3=E9=97=ADDialog=E5=90=8E=EF=BC=8C=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=88=90=E5=8A=9F=E5=90=8E=E4=BE=9D=E7=84=B6=E4=BC=9A=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E7=94=A8=E6=88=B7=E5=B7=B2=E7=BB=8F=E5=8F=96=E6=B6=88?= =?UTF-8?q?=E7=9A=84=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/EditServerPane.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 08436763803..4566260712e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -276,11 +276,13 @@ private void onAdd(Server.ServerPackStatus packStatus, String serverName, String case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); }); } else { - fireEvent(new DialogCloseEvent()); - if (reference != null) { - handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, packStatus)); - } else { - handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); + if (getScene() != null) { + fireEvent(new DialogCloseEvent()); + if (reference != null) { + handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, packStatus)); + } else { + handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); + } } } }).start(); From 7d470130d5da9a439984b1e9130d0e0d5cb92e8c Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 22:05:56 +0800 Subject: [PATCH 85/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=93=8D=E4=BD=9C=E4=BA=86=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E7=9A=84=E6=B8=B8=E6=88=8F=E5=AE=9E=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 2 +- .../ui/instances/server/ServerListPage.java | 4 ++-- .../ui/instances/server/ServerStatusPane.java | 23 ++++++++++--------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index 777bf97a00c..0b08cd8da69 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -263,7 +263,7 @@ public Decorator(Node mainPage, Consumer stageInitializer) { // Setup Minecraft server DnD root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( - server -> Controllers.dialog(new ServerStatusPane(null, server, true)))); + server -> Controllers.dialog(new ServerStatusPane(null, server, true, null)))); } /// Returns the node that must be installed as the scene root. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 8a5f297f9a1..269138a9d7d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -289,7 +289,7 @@ private void updateServerList() { getItems().setAll(stream.toList()); } - public static final class ServerListItem { + public final class ServerListItem { private final @NotNull ServerStorage.ServerStorageEntry storageEntry; private final Image serverFaviconImage; @@ -311,7 +311,7 @@ public Server getServer() { } public void openServerStatusPane() { - Controllers.dialog(new ServerStatusPane(observableServerStatus, getServer())); + Controllers.dialog(new ServerStatusPane(observableServerStatus, getServer(), ServerListPage.this.gameInstance)); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index bfc27c18486..6a99d465b3d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -30,7 +30,6 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; @@ -56,13 +55,19 @@ public class ServerStatusPane extends TransitionPane implements DialogAware { private final Label lblErrorMessage = new Label(); private final ObservableServerStatus observableServerStatus; private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + private final @Nullable HMCLGameInstance currentGameInstance; - public ServerStatusPane(ObservableServerStatus reference, Server server) { - this(reference, server, false); + public ServerStatusPane(ObservableServerStatus reference, Server server, @Nullable HMCLGameInstance gameInstance) { + this(reference, server, false, gameInstance); } - public ServerStatusPane(ObservableServerStatus reference, Server server, boolean fromDnD) { + public ServerStatusPane(ObservableServerStatus reference, Server server, boolean fromDnD, @Nullable HMCLGameInstance gameInstance) { this.server = server; + if (gameInstance == null) { + this.currentGameInstance = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); + } else { + this.currentGameInstance = gameInstance; + } this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(server.ip())); getStyleClass().add("skin-pane"); @@ -88,9 +93,7 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean addToInstallBtn.setOnAction(ignored0 -> { fireEvent(new DialogCloseEvent()); // open server manager - HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - HMCLGameInstance selectedInstance = repository.getSelectedInstance(); - if (selectedInstance == null) { + if (currentGameInstance == null) { JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download")); gotoDownload.getStyleClass().add("dialog-accept"); gotoDownload.setOnAction(ignored1 -> Controllers.navigate(Controllers.getDownloadPage())); @@ -101,7 +104,7 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean null); } else { if (Controllers.getGameInstancePage().getScene() == null) { - Instances.modifyServerList(selectedInstance); + Instances.modifyServerList(currentGameInstance); } if (!(Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage)) { @@ -121,9 +124,7 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean launchBtn.getStyleClass().add("dialog-accept"); launchBtn.setOnAction(e -> { fireEvent(new DialogCloseEvent()); - HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - HMCLGameInstance selectedInstance = repository.getSelectedInstance(); - Instances.launchAndEnterServer(selectedInstance, server.ip()); + Instances.launchAndEnterServer(currentGameInstance, server.ip()); }); actions.getChildren().add(launchBtn); From 3a2658d9279608d3c05fcca84c6b31ae4abd4d99 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 22:47:45 +0800 Subject: [PATCH 86/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8E=9F=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E8=A7=A3=E6=9E=90=E5=A4=B1=E8=B4=A5=E4=BC=9A=E8=B7=B3?= =?UTF-8?q?=E8=BF=87=20SRV=20=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/MojangBlockServerChecker.java | 221 +++++++++++------- .../jackhuang/hmcl/util/ServerAddress.java | 34 ++- 2 files changed, 158 insertions(+), 97 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java index ef9ba81185e..902d0fb29ed 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/resolver/MojangBlockServerChecker.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.server.resolver; +import org.jackhuang.hmcl.util.HostAndPort; import org.jetbrains.annotations.NotNull; import java.io.BufferedReader; @@ -28,117 +29,157 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.util.*; -import java.util.function.Predicate; import java.util.stream.Collectors; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class MojangBlockServerChecker { - private static volatile Predicate predicate; + private static volatile Checker checker; private MojangBlockServerChecker() { } - public static boolean isBlocking(InetSocketAddress inetSocketAddress0) { - if (predicate == null) { + public static boolean isBlocking(HostAndPort hostAndPort) { + if (checker == null) { synchronized (MojangBlockServerChecker.class) { - if (predicate == null) { - try { - URLConnection urlConnection = (new URL("https://sessionserver.mojang.com/blockedservers")).openConnection(); - urlConnection.setConnectTimeout(7_000); - urlConnection.setReadTimeout(7_000); - - try (InputStream is = urlConnection.getInputStream(); - BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1)) - ) { - Set collected = reader.lines().collect(Collectors.toUnmodifiableSet()); - - MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1"); - - predicate = new Predicate<>() { - private boolean isBlocking(@NotNull String server) { - if (server.startsWith("_minecraft._tcp.")) { - server = server.substring("_minecraft._tcp.".length()); - } - - while (server.charAt(server.length() - 1) == '.') { - server = server.substring(0, server.length() - 1); - } - - - if (isBlockedServerHostName(collected, server)) { - return true; - } else { - List parts = new ArrayList<>(Arrays.asList(server.split("\\."))); - boolean isIPV4 = isIPv4(parts); - if (!isIPV4 && isBlockedServerHostName(collected, "*." + server)) { - return true; - } - while (parts.size() > 1) { - parts.remove(isIPV4 ? parts.size() - 1 : 0); - String starredPart = isIPV4 ? String.join(".", parts) + ".*" : "*." + String.join(".", parts); - if (isBlockedServerHostName(collected, starredPart)) { - return true; - } - } - - return false; - } - } + if (checker == null) { + checker = createChecker(); + } + return checker.isBlocking(hostAndPort); + } + } else { + return checker.isBlocking(hostAndPort); + } + } + + public static boolean isBlocking(InetSocketAddress inetSocketAddress) { + if (checker == null) { + synchronized (MojangBlockServerChecker.class) { + if (checker == null) { + checker = createChecker(); + } + return checker.isBlocking(inetSocketAddress); + } + } else { + return checker.isBlocking(inetSocketAddress); + } + } + + private static @NotNull Checker createChecker() { + try { + URLConnection urlConnection = (new URL("https://sessionserver.mojang.com/blockedservers")).openConnection(); + urlConnection.setConnectTimeout(7_000); + urlConnection.setReadTimeout(7_000); + + try (InputStream is = urlConnection.getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.ISO_8859_1)) + ) { + Set collected = reader.lines().collect(Collectors.toUnmodifiableSet()); + MessageDigest sha1MessageDigest = MessageDigest.getInstance("SHA-1"); + + return new Checker() { + + private boolean isBlockedServerHostName(String server) { + byte[] serverBytes = server.toLowerCase(Locale.ROOT).getBytes(StandardCharsets.ISO_8859_1); + byte[] hash; + synchronized (sha1MessageDigest) { + hash = sha1MessageDigest.digest(serverBytes); + } + StringBuilder hexString = new StringBuilder(); + for (byte b : hash) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return collected.contains(hexString.toString()); + } + + private boolean isBlocking(@NotNull String server) { + if (server.startsWith("_minecraft._tcp.")) { + server = server.substring("_minecraft._tcp.".length()); + } + + while (server.charAt(server.length() - 1) == '.') { + server = server.substring(0, server.length() - 1); + } - private boolean isIPv4(List address) { - if (address.size() != 4) { - return false; - } - - for (String s : address) { - try { - int part = Integer.parseInt(s); - if (part < 0 || part > 255) { - return false; - } - } catch (NumberFormatException ignore) { - return false; - } - } + + if (isBlockedServerHostName(server)) { + return true; + } else { + List parts = new ArrayList<>(Arrays.asList(server.split("\\."))); + boolean isIPV4 = isIPv4(parts); + if (!isIPV4 && isBlockedServerHostName("*." + server)) { + return true; + } + while (parts.size() > 1) { + parts.remove(isIPV4 ? parts.size() - 1 : 0); + String starredPart = isIPV4 ? String.join(".", parts) + ".*" : "*." + String.join(".", parts); + if (isBlockedServerHostName(starredPart)) { return true; } + } - private boolean isBlockedServerHostName(@NotNull Set collect, String server) { - byte[] serverBytes = server.toLowerCase(Locale.ROOT).getBytes(StandardCharsets.ISO_8859_1); - byte[] hash; - synchronized (sha1MessageDigest) { - hash = sha1MessageDigest.digest(serverBytes); - } - StringBuilder hexString = new StringBuilder(); - for (byte b : hash) { - String hex = Integer.toHexString(0xff & b); - if (hex.length() == 1) { - hexString.append('0'); - } - hexString.append(hex); - } - return collect.contains(hexString.toString()); - } + return false; + } + } + + private boolean isIPv4(List address) { + if (address.size() != 4) { + return false; + } - @Override - public boolean test(InetSocketAddress inetSocketAddress) { - if (isBlocking(inetSocketAddress.getAddress().getHostName())) { - return true; - } - return isBlocking(inetSocketAddress.getAddress().getHostAddress()); + for (String s : address) { + try { + int part = Integer.parseInt(s); + if (part < 0 || part > 255) { + return false; } - }; + } catch (NumberFormatException ignore) { + return false; + } } + return true; + } + + @Override + public boolean isBlocking(HostAndPort hostAndPort) { - } catch (Exception e) { - LOG.error("Failed to initialize block server checker", e); - predicate = (hostAndIp) -> false; + return isBlocking(hostAndPort.host()); } - } + + @Override + public boolean isBlocking(InetSocketAddress address) { + if (isBlocking(address.getAddress().getHostAddress())) { + return true; + } + return isBlocking(address.getAddress().getHostName()); + } + }; } + + } catch (Exception e) { + LOG.error("Failed to initialize block server checker", e); + return new Checker() { + @Override + public boolean isBlocking(HostAndPort hostAndPort) { + return false; + } + + @Override + public boolean isBlocking(InetSocketAddress address) { + return false; + } + }; } - return predicate.test(inetSocketAddress0); + } + + private interface Checker { + boolean isBlocking(HostAndPort hostAndPort); + + boolean isBlocking(InetSocketAddress address); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java index 6f114ad4d5c..f9617d200cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ServerAddress.java @@ -97,22 +97,42 @@ public ServerAddress(@NotNull String host, int port) { } public ServerAddressResolveResult resolve() { + InetSocketAddress resolvedAddress = null; + UnknownHostException originalResolveException = null; try { - InetSocketAddress inetSocketAddress = hostAndPort.createInetSocketAddress(); - if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + resolvedAddress = hostAndPort.createInetSocketAddress(); + } catch (UnknownHostException e) { + originalResolveException = e; + } + + if (resolvedAddress != null) { + if (MojangBlockServerChecker.isBlocking(resolvedAddress)) { return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); } - HostAndPort lookupDnsResult = ServerDnsSrvRedirector.lookup(this); - if (lookupDnsResult != null) { - inetSocketAddress = lookupDnsResult.createInetSocketAddress(); - if (MojangBlockServerChecker.isBlocking(inetSocketAddress)) { + } + + if (MojangBlockServerChecker.isBlocking(this.hostAndPort())) { + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); + } + + try { + HostAndPort lookupDnsResultHostAndPort = ServerDnsSrvRedirector.lookup(this); + if (lookupDnsResultHostAndPort != null) { + InetSocketAddress redirectResolvedAddress = lookupDnsResultHostAndPort.createInetSocketAddress(); + if (MojangBlockServerChecker.isBlocking(redirectResolvedAddress)) { return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.BLOCKED_BY_MOJANG, new ServerBlockedByMojangException("blocked by mojang block server")); } + return ServerAddressResolveResult.succeed(this, redirectResolvedAddress); } - return ServerAddressResolveResult.succeed(this, inetSocketAddress); } catch (UnknownHostException e) { return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST, e); } + + if (resolvedAddress != null) { + return ServerAddressResolveResult.succeed(this, resolvedAddress); + } else { + return ServerAddressResolveResult.failed(this, ServerAddressResolveResult.FailureResult.Reason.UNKNOWN_HOST, originalResolveException); + } } public String toServerIp(boolean containQueryArgs) { From c90d6acc4464339549c6203a1b5dccc0f9c096b7 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Mon, 12 Oct 2026 23:09:59 +0800 Subject: [PATCH 87/97] =?UTF-8?q?=E7=BB=86=E8=8A=82=E6=96=B9=E9=9D=A2?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/EditServerPane.java | 11 +++++++++-- .../hmcl/ui/instances/server/ServerListPage.java | 4 +++- .../main/java/org/jackhuang/hmcl/server/Server.java | 4 ---- .../java/org/jackhuang/hmcl/server/ServerStorage.java | 3 +-- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 4566260712e..18c3951dcbc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -223,7 +223,10 @@ protected boolean computeValue() { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, serverPackStatus)); + handleCallback.accept(reference + .withIpAndName(serverIP, serverName) + .withPackStatus(serverPackStatus) + ); } else { handleCallback.accept(new Server(serverPackStatus, false, null, serverIP, serverName)); } @@ -279,7 +282,11 @@ private void onAdd(Server.ServerPackStatus packStatus, String serverName, String if (getScene() != null) { fireEvent(new DialogCloseEvent()); if (reference != null) { - handleCallback.accept(reference.withIpNameAndPackStatus(serverIP, serverName, packStatus)); + handleCallback.accept(reference + .withIpAndName(serverIP, serverName) + .withPackStatus(packStatus) + .withIcon(status.favicon()) + ); } else { handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 269138a9d7d..ad5187f9992 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -131,7 +131,9 @@ public void editServer(ServerListItem item) { ServerStorage storage = item.storageEntry.rootStorage(); return storage.updateEntry(item.storageEntry, oldValue -> oldValue .withIpAndName(server.ip(), server.name()) - .withPackStatus(server.serverPackStatus())); + .withIcon(server.icon()) + .withPackStatus(server.serverPackStatus()) + ); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { if (exception != null) LOG.warning("Failed to save server data.", exception); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 7e864bf2205..78179bd7859 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -118,10 +118,6 @@ public enum ServerPackStatus { return new Server(serverPackStatus, hidden, icon, ip, name); } - public @NotNull Server withIpNameAndPackStatus(@Nullable String ip, @Nullable String name, @NotNull ServerPackStatus serverPackStatus) { - return new Server(serverPackStatus, hidden, icon, ip, name); - } - public @NotNull Server withPackStatus(@NotNull ServerPackStatus serverPackStatus) { return new Server(serverPackStatus, hidden, icon, ip, name); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java index 68b4c8bc91a..3a680116f68 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; @@ -33,7 +32,7 @@ public class ServerStorage { private final Lock handleLock = new ReentrantLock(); - private final List holdInstances = new ArrayList<>(); + private final List holdInstances = new CopyOnWriteArrayList<>(); private final Path serverDatFilePath; private final List storageEntries; private volatile boolean storageValid = true; From 7e6054012347d8f903029d8af7b1fd9137b14db7 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 00:43:30 +0800 Subject: [PATCH 88/97] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=9B=BE=E6=A0=87=E4=BC=9A=E8=A6=86=E7=9B=96=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E4=B8=AD=E7=9A=84=E6=9C=8D=E5=8A=A1=E5=99=A8=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 124 ++++++++++++------ .../ui/instances/server/ServerStatusPane.java | 2 +- .../resources/assets/lang/I18N.properties | 2 + .../resources/assets/lang/I18N_ar.properties | 2 + .../resources/assets/lang/I18N_de.properties | 2 + .../resources/assets/lang/I18N_es.properties | 2 + .../resources/assets/lang/I18N_ja.properties | 2 + .../resources/assets/lang/I18N_lzh.properties | 2 + .../resources/assets/lang/I18N_ru.properties | 2 + .../resources/assets/lang/I18N_uk.properties | 2 + .../assets/lang/I18N_zh_Hans.properties | 2 + .../assets/lang/I18N_zh_Hant.properties | 2 + .../DatFileModifiedExternalException.java | 33 +++++ .../server/{ => sorage}/ServerStorage.java | 65 ++++++++- 14 files changed, 204 insertions(+), 40 deletions(-) create mode 100644 HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{ => sorage}/ServerStorage.java (70%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index ad5187f9992..a54d3567eda 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -42,7 +42,8 @@ import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.server.ServerStorage; +import org.jackhuang.hmcl.server.sorage.DatFileModifiedExternalException; +import org.jackhuang.hmcl.server.sorage.ServerStorage; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; @@ -135,8 +136,14 @@ public void editServer(ServerListItem item) { .withPackStatus(server.serverPackStatus()) ); }).whenComplete(Schedulers.javafx(), (updated, exception) -> { - if (exception != null) + if (exception != null) { LOG.warning("Failed to save server data.", exception); + } + + if (exception instanceof DatFileModifiedExternalException e) { + showFileModifiedExternalWarnDialog(e); + return; + } if (updated != null && updated) { int index = serverListEntries.indexOf(item); @@ -146,7 +153,6 @@ public void editServer(ServerListItem item) { updateServerList(); } - }).start(); } }))); @@ -163,12 +169,21 @@ private void delete(ServerListItem... items) { try { item.storageEntry.delete(); result.add(item); + } catch (DatFileModifiedExternalException e) { + throw e; } catch (IOException e) { LOG.error("Failed to delete server entry.", e); } } return result; }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + LOG.warning("Failed to delete server data.", exception); + } + if (exception instanceof DatFileModifiedExternalException e) { + showFileModifiedExternalWarnDialog(e); + return; + } if (result == null || result.isEmpty()) return; serverListEntries.removeAll(result); updateServerList(); @@ -177,26 +192,48 @@ private void delete(ServerListItem... items) { ); } + private void showFileModifiedExternalWarnDialog(@NotNull DatFileModifiedExternalException e) { + Controllers.dialog(new MessageDialogPane.Builder(i18n("server.manage.error.file.modified"), null, MessageDialogPane.MessageType.ERROR) + .addAction(i18n("server.manage.error.file.modified.overwrite"), () -> { + Task.runAsync(Schedulers.io(), () -> { + e.getStorage().forceOverwrite(); + }).whenComplete(Schedulers.javafx(), (result, exception) -> { + if (exception != null) { + LOG.warning("Failed to overwrite server data.", exception); + } + refresh(); + }).start(); + }) + .addCancel(this::refresh) + .build() + ); + } + public void copyServerIp(ServerListItem item) { FXUtils.copyText(item.getServer().ip(), i18n("server.manage.copy.server.ip.ok.toast")); } public void copyToInstance(ServerListItem item) { - addServer(item.getServer()); + if (gameInstance != null) { + addServer(item.getServer(), gameInstance); + } } public void addServer(Server server) { - HMCLGameInstance gameInstance = this.gameInstance; - if (gameInstance == null) return; + if (gameInstance != null) { + addServer(server, gameInstance); + } + } + public void addServer(Server server, @NotNull HMCLGameInstance toInstance) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage.ServerStorageEntry storageEntry; synchronized (serversFileHandleLockObject) { - ServerStorage storage = serverStorageMap.get(gameInstance.getServersDatFilePath()); + ServerStorage storage = serverStorageMap.get(toInstance.getServersDatFilePath()); if (storage == null) { - storage = new ServerStorage(gameInstance.getServersDatFilePath()); - storage.getHoldInstances().add(gameInstance); - serverStorageMap.put(gameInstance.getServersDatFilePath(), storage); + storage = new ServerStorage(toInstance.getServersDatFilePath()); + storage.getHoldInstances().add(toInstance); + serverStorageMap.put(toInstance.getServersDatFilePath(), storage); } storageEntry = storage.add(server); } @@ -206,6 +243,11 @@ public void addServer(Server server) { LOG.warning("Failed to save server data.", exception); } + if (exception instanceof DatFileModifiedExternalException e) { + showFileModifiedExternalWarnDialog(e); + return; + } + if (storageEntry != null) { serverListEntries.add(new ServerListItem(storageEntry)); updateServerList(); @@ -473,34 +515,40 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res // update latest server icon serverIcon.setImage(parseServerFaviconImageOrDefault(serverStatus.favicon())); - if (!Objects.equals(serverStatus.favicon(), item.getServer().icon())) { - // save latest icon - Task.supplyAsync(Schedulers.io(), () -> { - ServerStorage storage = item.storageEntry.rootStorage(); - return storage.updateEntry(item.storageEntry, server -> { - if (Objects.equals( - ServerAddress.parse(Objects.requireNonNull(server.ip())), - ((ServerStatusResult.SuccessResult) result).getRawServerAddress()) - ) { - return server.withIcon(serverStatus.favicon()); - } - return server; - }); - }).whenComplete(Schedulers.javafx(), (updated, exception) -> { - if (exception != null) - LOG.warning("Failed to save server data.", exception); - - if (updated != null && updated) { - int index = serverListEntries.indexOf(item); - if (index != -1) { - serverListEntries.set(index, new ServerListItem(item.storageEntry, item.observableServerStatus)); - - updateServerList(); - } - } - - }).start(); - } +// if (!Objects.equals(serverStatus.favicon(), item.getServer().icon())) { +// // save latest icon +// Task.supplyAsync(Schedulers.io(), () -> { +// ServerStorage storage = item.storageEntry.rootStorage(); +// return storage.updateEntry(item.storageEntry, server -> { +// if (Objects.equals( +// ServerAddress.parse(Objects.requireNonNull(server.ip())), +// ((ServerStatusResult.SuccessResult) result).getRawServerAddress()) +// ) { +// return server.withIcon(serverStatus.favicon()); +// } +// return server; +// }); +// }).whenComplete(Schedulers.javafx(), (updated, exception) -> { +// if (exception != null) { +// LOG.warning("Failed to save server data.", exception); +// } +// +// if (exception instanceof DatFileModifiedExternalException e) { +// showFileModifiedExternalWarnDialog(e); +// return; +// } +// +// if (updated != null && updated) { +// int index = serverListEntries.indexOf(item); +// if (index != -1) { +// serverListEntries.set(index, new ServerListItem(item.storageEntry, item.observableServerStatus)); +// +// updateServerList(); +// } +// } +// +// }).start(); +// } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index 6a99d465b3d..a9bd6b49592 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -113,7 +113,7 @@ public ServerStatusPane(ObservableServerStatus reference, Server server, boolean ServerListPage serverListPage = Controllers.getGameInstancePage().getServerListTab().getNode(); if (serverListPage != null) { - serverListPage.addServer(server); + serverListPage.addServer(server, currentGameInstance); } } }); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index b510fbd6339..e90eee84733 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1408,6 +1408,8 @@ server.manage.edit=Edit Server server.manage.edit.accept=Submit server.manage.edit.head=Edit Server server.manage.edit.still=Still Submit +server.manage.error.file.modified=The file being operated on has been externally modified +server.manage.error.file.modified.overwrite=Forced Overwrite server.manage.status=Server Status server.manage.status.error=Unable to fetch the current server status server.manage.status.error.blocked=The current server has been blocked diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 951fba7ae08..f1a527f539f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -1407,6 +1407,8 @@ server.manage.edit=تعديل الخادم server.manage.edit.accept=إرسال server.manage.edit.head=تعديل الخادم server.manage.edit.still=الإرسال على أي حال +server.manage.error.file.modified=تم تعديل الملف الجاري التعامل معه خارجيًا +server.manage.error.file.modified.overwrite=الكتابة القسرية server.manage.status=حالة الخادم server.manage.status.error=غير قادر على الحصول على حالة الخادم الحالية server.manage.status.error.blocked=تم حظر الخادم الحالي diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 648a1271a26..1455cd5d012 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -1406,6 +1406,8 @@ server.manage.edit=Server bearbeiten server.manage.edit.accept=Übermitteln server.manage.edit.head=Server bearbeiten server.manage.edit.still=Trotzdem Übermitteln +server.manage.error.file.modified=Die bearbeitete Datei wurde von außerhalb geändert +server.manage.error.file.modified.overwrite=Erzwingen des Überschreibens server.manage.status=Serverstatus server.manage.status.error=Der aktuelle Serverstatus kann nicht abgerufen werden server.manage.status.error.blocked=Der aktuelle Server wurde blockiert diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 38b4d4df268..9624a365287 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -1399,6 +1399,8 @@ server.manage.edit=Editar servidor server.manage.edit.accept=Enviar server.manage.edit.head=Editar servidor server.manage.edit.still=Enviar igualmente +server.manage.error.file.modified=El archivo que se está operando ha sido modificado externamente +server.manage.error.file.modified.overwrite=Sobrescritura forzada server.manage.status=Serveri seisund server.manage.status.error=Serveri praeguse seisundi leidmine nurjus server.manage.status.error.blocked=Praegune server on blokeeritud diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index e214394136e..4399339fe88 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -1203,6 +1203,8 @@ server.manage.edit=サーバーを編集 server.manage.edit.accept=送信 server.manage.edit.head=サーバーを編集 server.manage.edit.still=強制送信 +server.manage.error.file.modified=操作対象のファイルが外部から変更されました +server.manage.error.file.modified.overwrite=強制上書き server.manage.status=サーバの状態 server.manage.status.error=現在のサーバステータスを取得できません server.manage.status.error.blocked=現在のサーバーはブロックされています diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index f0c7189f661..29eb97ee0c3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -1215,6 +1215,8 @@ server.address.malformed=伺服器地址不合法 # server.manage.edit.accept= # server.manage.edit.head= # server.manage.edit.still= +# server.manage.error.file.modified= +# server.manage.error.file.modified.overwrite= # server.manage.status= # server.manage.status.error= # server.manage.status.error.blocked= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 527d071d52f..77092c8ca75 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -1339,6 +1339,8 @@ server.manage.edit=Редактировать сервер server.manage.edit.accept=Подтвердить server.manage.edit.head=Редактировать сервер server.manage.edit.still=Все равно подтвердить +server.manage.error.file.modified=Файл, с которым ведётся работа, был изменён извне +server.manage.error.file.modified.overwrite=Принудительная перезапись server.manage.status=Статус сервера server.manage.status.error=Не удалось получить текущее состояние сервера server.manage.status.error.blocked=Текущий сервер был заблокирован diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 34b302575e8..177649d46f7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -1362,6 +1362,8 @@ server.manage.edit=Редагувати сервер server.manage.edit.accept=Підтвердити server.manage.edit.head=Редагувати сервер server.manage.edit.still=Все одно підтвердити +server.manage.error.file.modified=Файл, з яким ведеться робота, було змінено зовнішньо +server.manage.error.file.modified.overwrite=Примусове перезаписування server.manage.status=Статус сервера server.manage.status.error=Не вдалося отримати поточний стан сервера server.manage.status.error.blocked=Поточний сервер заблоковано 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 54a8e4b7f70..9f4bb6219a7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1220,6 +1220,8 @@ server.manage.edit=编辑服务器 server.manage.edit.accept=提交 server.manage.edit.head=编辑服务器 server.manage.edit.still=依然提交 +server.manage.error.file.modified=所操作的文件已被外部修改 +server.manage.error.file.modified.overwrite=强制覆写 server.manage.status=服务器状态 server.manage.status.error=无法获取目标服务器状态 server.manage.status.error.blocked=目标服务器已被禁止 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 30569ea5c5a..da03b864c09 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1214,6 +1214,8 @@ server.manage.edit=編輯伺服器 server.manage.edit.accept=提交 server.manage.edit.head=編輯伺服器 server.manage.edit.still=仍然提交 +server.manage.error.file.modified=所操作的檔案已被外部修改 +server.manage.error.file.modified.overwrite=強制覆寫 server.manage.status=伺服器狀態 server.manage.status.error=無法取得目標伺服器狀態 server.manage.status.error.blocked=目標伺服器已被封鎖 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java new file mode 100644 index 00000000000..4790da9c559 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java @@ -0,0 +1,33 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.server.sorage; + +import java.io.IOException; + +public class DatFileModifiedExternalException extends IOException { + private final ServerStorage storage; + + public DatFileModifiedExternalException(ServerStorage storage, String message) { + super(message); + this.storage = storage; + } + + public ServerStorage getStorage() { + return storage; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java similarity index 70% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java index 3a680116f68..1c60d390b67 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java @@ -15,30 +15,39 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server; +package org.jackhuang.hmcl.server.sorage; import org.jackhuang.hmcl.game.GameInstance; +import org.jackhuang.hmcl.server.Server; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.UnaryOperator; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + public class ServerStorage { private final Lock handleLock = new ReentrantLock(); private final List holdInstances = new CopyOnWriteArrayList<>(); private final Path serverDatFilePath; private final List storageEntries; + private static volatile HashCalcer hashCalcer; private volatile boolean storageValid = true; + private byte[] currentFileHash; public ServerStorage(Path serverDatFilePath) throws IOException { this.serverDatFilePath = serverDatFilePath; + this.currentFileHash = calcServerDatFileHash(); handleLock.lock(); try { @@ -57,6 +66,37 @@ public ServerStorage(Path serverDatFilePath) throws IOException { } } + private HashCalcer createMd5Calcer() { + try { + MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5"); + return file -> { + if (Files.exists(file)) { + byte[] hash; + byte[] bytes = Files.readAllBytes(file); + synchronized (md5MessageDigest) { + hash = md5MessageDigest.digest(bytes); + } + return hash; + } + return new byte[0]; + }; + } catch (NoSuchAlgorithmException e) { + LOG.error("Failed to create md5 calcer.", e); + return bytes -> new byte[0]; + } + } + + private byte[] calcServerDatFileHash() throws IOException { + if (hashCalcer == null) { + synchronized (ServerStorage.class) { + if (hashCalcer == null) { + hashCalcer = createMd5Calcer(); + } + } + } + return hashCalcer.calc(serverDatFilePath); + } + public List getHoldInstances() { return holdInstances; } @@ -70,7 +110,19 @@ private void save() throws IOException { try { if (!storageValid) return; + checkHash(); + Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); + currentFileHash = calcServerDatFileHash(); + } finally { + handleLock.unlock(); + } + } + + public void forceOverwrite() throws IOException { + handleLock.lock(); + try { Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); + currentFileHash = calcServerDatFileHash(); } finally { handleLock.unlock(); } @@ -152,6 +204,12 @@ public boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull Serv } } + public void checkHash() throws IOException { + if (!Arrays.equals(currentFileHash, calcServerDatFileHash())) { + throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); + } + } + public class ServerStorageEntry { private volatile Server server; @@ -175,4 +233,9 @@ public Path getServerDatFilePath() { return serverDatFilePath; } } + + @FunctionalInterface + private interface HashCalcer { + byte[] calc(Path file) throws IOException; + } } From d064fc03f269e0d6ac728640caa8df64ec518fcf Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 00:47:10 +0800 Subject: [PATCH 89/97] update ServerListPage --- .../org/jackhuang/hmcl/ui/instances/server/ServerListPage.java | 1 - 1 file changed, 1 deletion(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index a54d3567eda..be657649d65 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -51,7 +51,6 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; -import org.jackhuang.hmcl.util.ServerAddress; import org.jackhuang.hmcl.util.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; From 6ce184b27337890bafc64c3c4acce822c1ba7a62 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 01:00:45 +0800 Subject: [PATCH 90/97] =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E5=BC=BA=E5=88=B6?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E8=83=BD=E8=BF=98=E5=8E=9F=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=9A=84=E6=89=80=E6=9C=89=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 2 +- .../DatFileModifiedExternalException.java | 11 ++++++++++- .../hmcl/server/sorage/ServerStorage.java | 17 ++++++++++------- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index be657649d65..6f04f2a731a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -195,7 +195,7 @@ private void showFileModifiedExternalWarnDialog(@NotNull DatFileModifiedExternal Controllers.dialog(new MessageDialogPane.Builder(i18n("server.manage.error.file.modified"), null, MessageDialogPane.MessageType.ERROR) .addAction(i18n("server.manage.error.file.modified.overwrite"), () -> { Task.runAsync(Schedulers.io(), () -> { - e.getStorage().forceOverwrite(); + e.getStorage().forceOverwrite(e.getDirtyServers()); }).whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception != null) { LOG.warning("Failed to overwrite server data.", exception); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java index 4790da9c559..d08720a69de 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java @@ -17,17 +17,26 @@ */ package org.jackhuang.hmcl.server.sorage; +import org.jackhuang.hmcl.server.Server; + import java.io.IOException; +import java.util.List; public class DatFileModifiedExternalException extends IOException { private final ServerStorage storage; + private final List dirtyServers; - public DatFileModifiedExternalException(ServerStorage storage, String message) { + public DatFileModifiedExternalException(ServerStorage storage, List dirtyServers, String message) { super(message); this.storage = storage; + this.dirtyServers = dirtyServers; } public ServerStorage getStorage() { return storage; } + + public List getDirtyServers() { + return dirtyServers; + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java index 1c60d390b67..440a5562fe3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java @@ -39,8 +39,8 @@ public class ServerStorage { private final Lock handleLock = new ReentrantLock(); private final List holdInstances = new CopyOnWriteArrayList<>(); + private final List storageEntries = new CopyOnWriteArrayList<>(); private final Path serverDatFilePath; - private final List storageEntries; private static volatile HashCalcer hashCalcer; private volatile boolean storageValid = true; private byte[] currentFileHash; @@ -51,15 +51,13 @@ public ServerStorage(Path serverDatFilePath) throws IOException { handleLock.lock(); try { + storageEntries.clear(); if (Files.exists(serverDatFilePath)) { List servers = Server.loadFromServersDat(serverDatFilePath); - storageEntries = new CopyOnWriteArrayList<>(); for (Server server : servers) { ServerStorageEntry entry = new ServerStorageEntry(server); storageEntries.add(entry); } - } else { - storageEntries = new CopyOnWriteArrayList<>(); } } finally { handleLock.unlock(); @@ -118,10 +116,15 @@ private void save() throws IOException { } } - public void forceOverwrite() throws IOException { + public void forceOverwrite(@NotNull List servers) throws IOException { handleLock.lock(); try { - Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); + storageEntries.clear(); + for (Server server : servers) { + ServerStorageEntry entry = new ServerStorageEntry(server); + storageEntries.add(entry); + } + Server.saveToServersDat(servers, serverDatFilePath); currentFileHash = calcServerDatFileHash(); } finally { handleLock.unlock(); @@ -206,7 +209,7 @@ public boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull Serv public void checkHash() throws IOException { if (!Arrays.equals(currentFileHash, calcServerDatFileHash())) { - throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); + throw new DatFileModifiedExternalException(this, storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath.toAbsolutePath().toString()); } } From 209b405100fdf252996cd9f04c79bbd666930e64 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 01:39:34 +0800 Subject: [PATCH 91/97] =?UTF-8?q?=E4=B8=8D=E4=BF=9D=E7=95=99=E8=A6=86?= =?UTF-8?q?=E7=9B=96=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/ServerListPage.java | 12 +----------- .../main/resources/assets/lang/I18N.properties | 1 - .../resources/assets/lang/I18N_ar.properties | 2 +- .../resources/assets/lang/I18N_de.properties | 2 +- .../resources/assets/lang/I18N_es.properties | 2 +- .../resources/assets/lang/I18N_ja.properties | 2 +- .../resources/assets/lang/I18N_lzh.properties | 2 +- .../resources/assets/lang/I18N_ru.properties | 2 +- .../resources/assets/lang/I18N_uk.properties | 2 +- .../assets/lang/I18N_zh_Hans.properties | 1 - .../assets/lang/I18N_zh_Hant.properties | 1 - .../DatFileModifiedExternalException.java | 11 +---------- .../hmcl/server/sorage/ServerStorage.java | 17 +---------------- 13 files changed, 10 insertions(+), 47 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 6f04f2a731a..110fbeb4a89 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -193,17 +193,7 @@ private void delete(ServerListItem... items) { private void showFileModifiedExternalWarnDialog(@NotNull DatFileModifiedExternalException e) { Controllers.dialog(new MessageDialogPane.Builder(i18n("server.manage.error.file.modified"), null, MessageDialogPane.MessageType.ERROR) - .addAction(i18n("server.manage.error.file.modified.overwrite"), () -> { - Task.runAsync(Schedulers.io(), () -> { - e.getStorage().forceOverwrite(e.getDirtyServers()); - }).whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - LOG.warning("Failed to overwrite server data.", exception); - } - refresh(); - }).start(); - }) - .addCancel(this::refresh) + .addCancel(i18n("button.refresh"), this::refresh) .build() ); } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index e90eee84733..8caeb164b96 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -1409,7 +1409,6 @@ server.manage.edit.accept=Submit server.manage.edit.head=Edit Server server.manage.edit.still=Still Submit server.manage.error.file.modified=The file being operated on has been externally modified -server.manage.error.file.modified.overwrite=Forced Overwrite server.manage.status=Server Status server.manage.status.error=Unable to fetch the current server status server.manage.status.error.blocked=The current server has been blocked diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index f1a527f539f..a647300b865 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -861,6 +861,7 @@ java.download.prompt=يُرجى اختيار إصدار Java الذي تريد java.download.distribution=التوزيع java.download.version=الإصدار java.download.packageType=نوع الحزمة +# java.empty= java.management=إدارة Java java.info.architecture=المعمارية java.info.vendor=المزوّد @@ -1408,7 +1409,6 @@ server.manage.edit.accept=إرسال server.manage.edit.head=تعديل الخادم server.manage.edit.still=الإرسال على أي حال server.manage.error.file.modified=تم تعديل الملف الجاري التعامل معه خارجيًا -server.manage.error.file.modified.overwrite=الكتابة القسرية server.manage.status=حالة الخادم server.manage.status.error=غير قادر على الحصول على حالة الخادم الحالية server.manage.status.error.blocked=تم حظر الخادم الحالي diff --git a/HMCL/src/main/resources/assets/lang/I18N_de.properties b/HMCL/src/main/resources/assets/lang/I18N_de.properties index 1455cd5d012..1f8eabaa46d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_de.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_de.properties @@ -859,6 +859,7 @@ java.download.prompt=Bitte wählen Sie die Java-Version, die Sie herunterladen m java.download.distribution=Distribution java.download.version=Version java.download.packageType=Pakettyp +# java.empty= java.management=Java-Verwaltung java.info.architecture=Architektur java.info.vendor=Anbieter @@ -1407,7 +1408,6 @@ server.manage.edit.accept=Übermitteln server.manage.edit.head=Server bearbeiten server.manage.edit.still=Trotzdem Übermitteln server.manage.error.file.modified=Die bearbeitete Datei wurde von außerhalb geändert -server.manage.error.file.modified.overwrite=Erzwingen des Überschreibens server.manage.status=Serverstatus server.manage.status.error=Der aktuelle Serverstatus kann nicht abgerufen werden server.manage.status.error.blocked=Der aktuelle Server wurde blockiert diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 9624a365287..b8975821132 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -853,6 +853,7 @@ java.download.prompt=Elija la versión de Java que desea descargar: java.download.distribution=Distribución java.download.version=Versión java.download.packageType=Tipo de paquete +# java.empty= java.management=Gestión Java java.info.architecture=Arquitectura java.info.vendor=Proveedor @@ -1400,7 +1401,6 @@ server.manage.edit.accept=Enviar server.manage.edit.head=Editar servidor server.manage.edit.still=Enviar igualmente server.manage.error.file.modified=El archivo que se está operando ha sido modificado externamente -server.manage.error.file.modified.overwrite=Sobrescritura forzada server.manage.status=Serveri seisund server.manage.status.error=Serveri praeguse seisundi leidmine nurjus server.manage.status.error.blocked=Praegune server on blokeeritud diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index 4399339fe88..78319090c6d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -663,6 +663,7 @@ java.download.prompt=ダウンロードしたいJavaのバージョンを選択 java.download.distribution=配布の種類 java.download.version=バージョン java.download.packageType=パッケージの種類 +# java.empty= java.management=Javaの管理 java.info.architecture=アーキテクチャ java.info.vendor=発行元 @@ -1204,7 +1205,6 @@ server.manage.edit.accept=送信 server.manage.edit.head=サーバーを編集 server.manage.edit.still=強制送信 server.manage.error.file.modified=操作対象のファイルが外部から変更されました -server.manage.error.file.modified.overwrite=強制上書き server.manage.status=サーバの状態 server.manage.status.error=現在のサーバステータスを取得できません server.manage.status.error.blocked=現在のサーバーはブロックされています diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 29eb97ee0c3..d952721c54d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -675,6 +675,7 @@ java.download.prompt=須擇一爪哇版以引: java.download.distribution=廣布版 java.download.version=版 java.download.packageType=裝包類 +# java.empty= java.management=司爪哇 java.info.architecture=大構 java.info.vendor=供者 @@ -1216,7 +1217,6 @@ server.address.malformed=伺服器地址不合法 # server.manage.edit.head= # server.manage.edit.still= # server.manage.error.file.modified= -# server.manage.error.file.modified.overwrite= # server.manage.status= # server.manage.status.error= # server.manage.status.error.blocked= diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index 77092c8ca75..7532341a731 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -790,6 +790,7 @@ java.download.prompt=Выберите версию Java, которую вы х java.download.distribution=Дистрибутив java.download.version=Версия java.download.packageType=Тип пакета +# java.empty= java.management=Управление Java java.info.architecture=Архитектура java.info.vendor=Поставщик @@ -1340,7 +1341,6 @@ server.manage.edit.accept=Подтвердить server.manage.edit.head=Редактировать сервер server.manage.edit.still=Все равно подтвердить server.manage.error.file.modified=Файл, с которым ведётся работа, был изменён извне -server.manage.error.file.modified.overwrite=Принудительная перезапись server.manage.status=Статус сервера server.manage.status.error=Не удалось получить текущее состояние сервера server.manage.status.error.blocked=Текущий сервер был заблокирован diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 177649d46f7..09801da125d 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -814,6 +814,7 @@ java.download.prompt=Будь ласка, виберіть версію Java, я java.download.distribution=Дистрибутив java.download.version=Версія java.download.packageType=Тип пакета +# java.empty= java.management=Керування Java java.info.architecture=Архітектура java.info.vendor=Постачальник @@ -1363,7 +1364,6 @@ server.manage.edit.accept=Підтвердити server.manage.edit.head=Редагувати сервер server.manage.edit.still=Все одно підтвердити server.manage.error.file.modified=Файл, з яким ведеться робота, було змінено зовнішньо -server.manage.error.file.modified.overwrite=Примусове перезаписування server.manage.status=Статус сервера server.manage.status.error=Не вдалося отримати поточний стан сервера server.manage.status.error.blocked=Поточний сервер заблоковано 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 9f4bb6219a7..e18aef466c7 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hans.properties @@ -1221,7 +1221,6 @@ server.manage.edit.accept=提交 server.manage.edit.head=编辑服务器 server.manage.edit.still=依然提交 server.manage.error.file.modified=所操作的文件已被外部修改 -server.manage.error.file.modified.overwrite=强制覆写 server.manage.status=服务器状态 server.manage.status.error=无法获取目标服务器状态 server.manage.status.error.blocked=目标服务器已被禁止 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 da03b864c09..c36571a8bf1 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_Hant.properties @@ -1215,7 +1215,6 @@ server.manage.edit.accept=提交 server.manage.edit.head=編輯伺服器 server.manage.edit.still=仍然提交 server.manage.error.file.modified=所操作的檔案已被外部修改 -server.manage.error.file.modified.overwrite=強制覆寫 server.manage.status=伺服器狀態 server.manage.status.error=無法取得目標伺服器狀態 server.manage.status.error.blocked=目標伺服器已被封鎖 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java index d08720a69de..4790da9c559 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java @@ -17,26 +17,17 @@ */ package org.jackhuang.hmcl.server.sorage; -import org.jackhuang.hmcl.server.Server; - import java.io.IOException; -import java.util.List; public class DatFileModifiedExternalException extends IOException { private final ServerStorage storage; - private final List dirtyServers; - public DatFileModifiedExternalException(ServerStorage storage, List dirtyServers, String message) { + public DatFileModifiedExternalException(ServerStorage storage, String message) { super(message); this.storage = storage; - this.dirtyServers = dirtyServers; } public ServerStorage getStorage() { return storage; } - - public List getDirtyServers() { - return dirtyServers; - } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java index 440a5562fe3..4bbf9df11a5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java @@ -116,21 +116,6 @@ private void save() throws IOException { } } - public void forceOverwrite(@NotNull List servers) throws IOException { - handleLock.lock(); - try { - storageEntries.clear(); - for (Server server : servers) { - ServerStorageEntry entry = new ServerStorageEntry(server); - storageEntries.add(entry); - } - Server.saveToServersDat(servers, serverDatFilePath); - currentFileHash = calcServerDatFileHash(); - } finally { - handleLock.unlock(); - } - } - public void invalid() { handleLock.lock(); try { @@ -209,7 +194,7 @@ public boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull Serv public void checkHash() throws IOException { if (!Arrays.equals(currentFileHash, calcServerDatFileHash())) { - throw new DatFileModifiedExternalException(this, storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath.toAbsolutePath().toString()); + throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); } } From dc245cf93a6e6250940f96ad2a06d59f8942bca6 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 02:02:52 +0800 Subject: [PATCH 92/97] =?UTF-8?q?=E6=B6=88=E9=99=A4=E5=8F=AF=E8=83=BD?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=E7=9A=84=E7=AB=9E=E6=80=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jackhuang/hmcl/server/Server.java | 14 ++---- .../hmcl/server/sorage/ServerStorage.java | 47 ++++++++++++------- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java index 78179bd7859..eab8b119759 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/Server.java @@ -19,12 +19,11 @@ import org.glavo.nbt.io.NBTCodec; import org.glavo.nbt.tag.*; -import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.IOException; -import java.nio.file.Path; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; @@ -53,10 +52,9 @@ public static Server loadSingle(@NotNull CompoundTag tag) { ); } - public static List loadFromServersDat(@NotNull Path file) throws IOException { + public static List loadFromServersDat(byte[] data) throws IOException { List servers = new ArrayList<>(); - - CompoundTag root = NBTCodec.of().readTag(file, TagType.COMPOUND); + CompoundTag root = NBTCodec.of().readTag(data, TagType.COMPOUND); Tag serversTag = root.get("servers"); if (serversTag == null) { throw new IOException("servers tag not found"); @@ -75,7 +73,7 @@ public static List loadFromServersDat(@NotNull Path file) throws IOExcep return servers; } - public static void saveToServersDat(@NotNull List servers, @NotNull Path file) throws IOException { + public static void saveServersDat(@NotNull List servers, OutputStream outputStream) throws IOException { ListTag tag = new ListTag<>(); for (Server server : servers) { CompoundTag serverTag = new CompoundTag(); @@ -86,9 +84,7 @@ public static void saveToServersDat(@NotNull List servers, CompoundTag root = new CompoundTag(); root.addTag("servers", tag); - FileUtils.saveSafely(file, outputStream -> - NBTCodec.of().writeTag(outputStream, root) - ); + NBTCodec.of().writeTag(outputStream, root); } public void writeToCompoundTag(CompoundTag tag) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java index 4bbf9df11a5..bb4e456771d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java @@ -19,9 +19,11 @@ import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.server.Server; +import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; @@ -43,17 +45,18 @@ public class ServerStorage { private final Path serverDatFilePath; private static volatile HashCalcer hashCalcer; private volatile boolean storageValid = true; - private byte[] currentFileHash; + private byte[] currentFileHash = new byte[0]; public ServerStorage(Path serverDatFilePath) throws IOException { this.serverDatFilePath = serverDatFilePath; - this.currentFileHash = calcServerDatFileHash(); handleLock.lock(); try { storageEntries.clear(); if (Files.exists(serverDatFilePath)) { - List servers = Server.loadFromServersDat(serverDatFilePath); + byte[] bytes = Files.readAllBytes(serverDatFilePath); + this.currentFileHash = calcServerDatFileHash(bytes); + List servers = Server.loadFromServersDat(bytes); for (Server server : servers) { ServerStorageEntry entry = new ServerStorageEntry(server); storageEntries.add(entry); @@ -67,16 +70,12 @@ public ServerStorage(Path serverDatFilePath) throws IOException { private HashCalcer createMd5Calcer() { try { MessageDigest md5MessageDigest = MessageDigest.getInstance("MD5"); - return file -> { - if (Files.exists(file)) { + return input -> { byte[] hash; - byte[] bytes = Files.readAllBytes(file); synchronized (md5MessageDigest) { - hash = md5MessageDigest.digest(bytes); + hash = md5MessageDigest.digest(input); } return hash; - } - return new byte[0]; }; } catch (NoSuchAlgorithmException e) { LOG.error("Failed to create md5 calcer.", e); @@ -84,7 +83,7 @@ private HashCalcer createMd5Calcer() { } } - private byte[] calcServerDatFileHash() throws IOException { + private byte[] calcServerDatFileHash(byte[] data) throws IOException { if (hashCalcer == null) { synchronized (ServerStorage.class) { if (hashCalcer == null) { @@ -92,7 +91,7 @@ private byte[] calcServerDatFileHash() throws IOException { } } } - return hashCalcer.calc(serverDatFilePath); + return hashCalcer.calc(data); } public List getHoldInstances() { @@ -109,8 +108,18 @@ private void save() throws IOException { if (!storageValid) return; checkHash(); - Server.saveToServersDat(storageEntries.stream().map(e -> e.server).toList(), serverDatFilePath); - currentFileHash = calcServerDatFileHash(); + + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + Server.saveServersDat(storageEntries.stream().map(e -> e.server).toList(), outputStream); + byte[] bytes = outputStream.toByteArray(); + + FileUtils.saveSafely(serverDatFilePath, op -> + op.write(bytes) + ); + + currentFileHash = calcServerDatFileHash(bytes); + } + } finally { handleLock.unlock(); } @@ -193,8 +202,14 @@ public boolean updateEntry(ServerStorageEntry entry, UnaryOperator<@NotNull Serv } public void checkHash() throws IOException { - if (!Arrays.equals(currentFileHash, calcServerDatFileHash())) { - throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); + if (Files.exists(serverDatFilePath)) { + if (!Arrays.equals(currentFileHash, calcServerDatFileHash(Files.readAllBytes(serverDatFilePath)))) { + throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); + } + } else { + if (!Arrays.equals(currentFileHash, new byte[0])) { + throw new DatFileModifiedExternalException(this, serverDatFilePath.toAbsolutePath().toString()); + } } } @@ -224,6 +239,6 @@ public Path getServerDatFilePath() { @FunctionalInterface private interface HashCalcer { - byte[] calc(Path file) throws IOException; + byte[] calc(byte[] input) throws IOException; } } From a2c772607a3c81ef5d042519aac1f9afc57b26a4 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 02:05:56 +0800 Subject: [PATCH 93/97] =?UTF-8?q?=E5=8C=85=E5=90=8D=E9=94=99=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jackhuang/hmcl/ui/instances/server/ServerListPage.java | 4 ++-- .../{sorage => storage}/DatFileModifiedExternalException.java | 2 +- .../hmcl/server/{sorage => storage}/ServerStorage.java | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{sorage => storage}/DatFileModifiedExternalException.java (96%) rename HMCLCore/src/main/java/org/jackhuang/hmcl/server/{sorage => storage}/ServerStorage.java (99%) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 110fbeb4a89..cec20a2d8a8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -42,8 +42,8 @@ import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.server.sorage.DatFileModifiedExternalException; -import org.jackhuang.hmcl.server.sorage.ServerStorage; +import org.jackhuang.hmcl.server.storage.DatFileModifiedExternalException; +import org.jackhuang.hmcl.server.storage.ServerStorage; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/DatFileModifiedExternalException.java similarity index 96% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/DatFileModifiedExternalException.java index 4790da9c559..23cddd6d31c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/DatFileModifiedExternalException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/DatFileModifiedExternalException.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server.sorage; +package org.jackhuang.hmcl.server.storage; import java.io.IOException; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/ServerStorage.java similarity index 99% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/ServerStorage.java index bb4e456771d..bef5a9f634c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/sorage/ServerStorage.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/storage/ServerStorage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.server.sorage; +package org.jackhuang.hmcl.server.storage; import org.jackhuang.hmcl.game.GameInstance; import org.jackhuang.hmcl.server.Server; From 58fa76d21f532085b37605eccdf50bb16472e071 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Tue, 13 Oct 2026 02:15:59 +0800 Subject: [PATCH 94/97] =?UTF-8?q?=E5=8A=A0=E5=9B=BAeditServerPane=E5=AF=B9?= =?UTF-8?q?cancel=E7=9A=84=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/instances/server/EditServerPane.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java index 18c3951dcbc..681093648dc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java @@ -64,10 +64,13 @@ public class EditServerPane extends TransitionPane implements DialogAware { private final SimpleBooleanProperty stillActionProgress = new SimpleBooleanProperty(false); + private boolean cancelled; + public EditServerPane(Type type, @Nullable Server reference, Consumer handleCallback) { this.type = type; this.reference = reference; this.handleCallback = handleCallback; + this.cancelled = false; getStyleClass().add("skin-pane"); @@ -279,23 +282,25 @@ private void onAdd(Server.ServerPackStatus packStatus, String serverName, String case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); }); } else { - if (getScene() != null) { - fireEvent(new DialogCloseEvent()); - if (reference != null) { - handleCallback.accept(reference - .withIpAndName(serverIP, serverName) - .withPackStatus(packStatus) - .withIcon(status.favicon()) - ); - } else { - handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); - } + if (getScene() == null || cancelled) { + return; + } + fireEvent(new DialogCloseEvent()); + if (reference != null) { + handleCallback.accept(reference + .withIpAndName(serverIP, serverName) + .withPackStatus(packStatus) + .withIcon(status.favicon()) + ); + } else { + handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); } } }).start(); } private void onCancel() { + cancelled = true; fireEvent(new DialogCloseEvent()); } From 53e9408f36855a6f760f645c7b29f444bce63078 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Thu, 15 Oct 2026 16:35:28 +0800 Subject: [PATCH 95/97] =?UTF-8?q?=E4=BD=BF=E7=94=A8ToobLac=E7=9A=84Subscri?= =?UTF-8?q?ption?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerListPage.java | 6 +++--- .../jackhuang/hmcl/util/javafx/Subscription.java | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index cec20a2d8a8..3f1f05aae57 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -37,7 +37,6 @@ import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.util.Duration; -import javafx.util.Subscription; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; @@ -52,6 +51,7 @@ import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.StringUtils; +import org.jackhuang.hmcl.util.javafx.Subscription; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -470,10 +470,10 @@ protected void updateControl(ServerListItem item, boolean empty) { item.observableServerStatus.refreshIfNoResultAsync(false); applyServerStatusResult(item, item.observableServerStatus.resultProperty().get()); - serverStatusResultValueSubscription = item.observableServerStatus.resultProperty().subscribe(result -> applyServerStatusResult(item, result)); + serverStatusResultValueSubscription = Subscription.subscribe(item.observableServerStatus.resultProperty(), result -> applyServerStatusResult(item, result)); applyServerPinging(item, item.observableServerStatus.pingingProperty().get()); - serverStatusPingingValueSubscription = item.observableServerStatus.pingingProperty().subscribe(pinging -> applyServerPinging(item, pinging)); + serverStatusPingingValueSubscription = Subscription.subscribe(item.observableServerStatus.pingingProperty(), pinging -> applyServerPinging(item, pinging)); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/Subscription.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/Subscription.java index 0f62397849e..f994ac9351e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/Subscription.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/javafx/Subscription.java @@ -19,9 +19,12 @@ import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import java.util.List; import java.util.Objects; +import java.util.function.Consumer; /// Alternate for JavaFX version below 21 @FunctionalInterface @@ -38,6 +41,16 @@ static Subscription subscribe(Observable observable, Runnable invalidationSubscr return () -> observable.removeListener(listener); } + static Subscription subscribe(ObservableValue observableValue, Consumer valueSubscriber) { + Objects.requireNonNull(valueSubscriber, "valueSubscriber cannot be null"); + ChangeListener listener = (obs, old, current) -> valueSubscriber.accept(current); + + observableValue.addListener(listener); + valueSubscriber.accept(observableValue.getValue()); + + return () -> observableValue.removeListener(listener); + } + static Subscription combine(Subscription... subscriptions) { List list = List.of(subscriptions); From fc7155b6f9f138e78314bb6a4d3c126aa60f8fb5 Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sat, 17 Oct 2026 23:53:18 +0800 Subject: [PATCH 96/97] =?UTF-8?q?=E5=B0=86=20networkLatency=20=E4=BB=8E=20?= =?UTF-8?q?ServerStatus=20=E7=A7=BB=E5=8A=A8=E5=88=B0=20ServerStatusResult?= =?UTF-8?q?=20=E5=86=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/instances/server/ServerListPage.java | 5 +++-- .../hmcl/ui/instances/server/ServerStatusPane.java | 2 +- .../org/jackhuang/hmcl/server/ServerStatus.java | 2 +- .../jackhuang/hmcl/server/ServerStatusResult.java | 13 ++++++++++--- .../server/pinger/LegacyServerStatusPinger.java | 5 ++--- .../server/pinger/ModernServerStatusPinger.java | 7 +++---- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java index 3f1f05aae57..f4d79e07ea2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java @@ -499,8 +499,9 @@ private void applyServerStatusResult(ServerListItem item, ServerStatusResult res case BLOCKED_BY_MOJANG -> i18n("server.manage.status.outside.error.blocked"); }); } else { - serverNetworkLatencyPane.pong(serverStatus.networkLatency()); - statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", serverStatus.networkLatency()))); + ServerStatusResult.SuccessResult successResult = (ServerStatusResult.SuccessResult) result; + serverNetworkLatencyPane.pong(successResult.getNetworkLatency()); + statusBtnTooltip.setText(i18n("server.manage.status.outside.pong", String.format("%,d", successResult.getNetworkLatency()))); // update latest server icon serverIcon.setImage(parseServerFaviconImageOrDefault(serverStatus.favicon())); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java index a9bd6b49592..87381772034 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java @@ -293,7 +293,7 @@ public void applyResult(ServerStatusResult statusResult) { } else { rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), i18n("server.playversion.unknown"), null, new Insets(0, 4, 0, 4))); } - rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", status.networkLatency()), null, new Insets(0, 4, 0, 4))); + rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", ((ServerStatusResult.SuccessResult) statusResult).getNetworkLatency()), null, new Insets(0, 4, 0, 4))); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java index 0e178162dcb..65e76e96481 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatus.java @@ -24,8 +24,8 @@ import java.util.List; import java.util.UUID; +// https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping#Status_Response public record ServerStatus( - long networkLatency, @Nullable Version version, @NotNull Players players, @NotNull JsonElement description, diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java index 286a1591a1b..9753ebf26d4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/ServerStatusResult.java @@ -19,11 +19,12 @@ import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jackhuang.hmcl.util.ServerAddress; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public abstract sealed class ServerStatusResult { - public static ServerStatusResult success(ServerAddress rawServerAddress, ServerStatusPinger pinger, ServerStatus status) { - return new SuccessResult(rawServerAddress, pinger, status); + public static ServerStatusResult success(@NotNull ServerAddress rawServerAddress, @NotNull ServerStatusPinger pinger, long networkLatency, @NotNull ServerStatus status) { + return new SuccessResult(rawServerAddress, pinger, networkLatency, status); } public static ServerStatusResult failure(FailureResult.Reason reason, Exception exception) { @@ -55,11 +56,13 @@ public boolean isFailure() { public static final class SuccessResult extends ServerStatusResult { private final ServerAddress rawServerAddress; private final ServerStatusPinger pinger; + private final long networkLatency; private final ServerStatus status; - private SuccessResult(ServerAddress rawServerAddress, ServerStatusPinger pinger, ServerStatus status) { + private SuccessResult(ServerAddress rawServerAddress, ServerStatusPinger pinger, long networkLatency, ServerStatus status) { this.rawServerAddress = rawServerAddress; this.pinger = pinger; + this.networkLatency = networkLatency; this.status = status; } @@ -74,6 +77,10 @@ public ServerStatusPinger getPinger() { public ServerStatus getServerStatus() { return status; } + + public long getNetworkLatency() { + return networkLatency; + } } public static final class FailureResult extends ServerStatusResult { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java index 72d983388dd..c2824aff138 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/LegacyServerStatusPinger.java @@ -74,7 +74,7 @@ private LegacyServerStatusPinger() { String response = readString(in); long networkLatency = System.currentTimeMillis() - sendTime; - return ServerStatusResult.success(successResult.getRawAddress(), this, parseResult(networkLatency, response)); + return ServerStatusResult.success(successResult.getRawAddress(), this, networkLatency, parseResult(response)); } } catch (Exception e) { return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); @@ -86,7 +86,7 @@ private LegacyServerStatusPinger() { return "Legacy Server Status Pinger"; } - private @NotNull ServerStatus parseResult(long networkLatency, @NotNull String input) throws IOException { + private @NotNull ServerStatus parseResult(@NotNull String input) throws IOException { ServerStatus.Version version; String motd; ServerStatus.Players players; @@ -122,7 +122,6 @@ private LegacyServerStatusPinger() { } return new ServerStatus( - networkLatency, version, players, new JsonPrimitive(motd), null, false, null); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java index 4d93c683ae9..06d912e95ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/server/pinger/ModernServerStatusPinger.java @@ -63,8 +63,8 @@ private ModernServerStatusPinger() { JsonObject rootStatus = JsonParser.parseString(status).getAsJsonObject(); - ServerStatus parseServerStatus = parseServerStatus(networkLatency, rootStatus); - return ServerStatusResult.success(successResult.getRawAddress(), this, parseServerStatus); + ServerStatus parseServerStatus = parseServerStatus(rootStatus); + return ServerStatusResult.success(successResult.getRawAddress(), this, networkLatency, parseServerStatus); } } catch (Exception e) { return ServerStatusResult.failure(ServerStatusResult.FailureResult.Reason.EXCEPTION, e); @@ -76,7 +76,7 @@ private ModernServerStatusPinger() { return "Modern Server Status Pinger"; } - private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatus) { + private ServerStatus parseServerStatus(@NotNull JsonObject rootStatus) { JsonObject versionJsonObject = rootStatus.getAsJsonObject("version"); ServerStatus.Version version = new ServerStatus.Version(versionJsonObject.get("name").getAsString(), versionJsonObject.get("protocol").getAsInt()); @@ -135,7 +135,6 @@ private ServerStatus parseServerStatus(long networkLatency, JsonObject rootStatu } return new ServerStatus( - networkLatency, version, players, Objects.requireNonNullElse(rootStatus.get("description"), JsonNull.INSTANCE), From 2347d664f702a32fa0aab618cc38af4b0c108a6d Mon Sep 17 00:00:00 2001 From: CaaMoe Date: Sun, 18 Oct 2026 00:13:53 +0800 Subject: [PATCH 97/97] =?UTF-8?q?=E5=B0=86server=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=9A=84dialog=20skin=20=E4=B8=80=E4=BA=9B=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E6=8E=A7=E4=BB=B6=E5=85=A8=E6=94=BE=E5=88=B0?= =?UTF-8?q?=20server=20page=20=E7=9A=84=E7=B1=BB=E9=87=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/ui/decorator/Decorator.java | 4 +- .../hmcl/ui/instances/GameInstancePage.java | 1 - .../{server => }/ServerListPage.java | 813 +++++++++++++++++- .../ui/instances/server/EditServerPane.java | 316 ------- .../server/ObservableServerStatus.java | 105 --- .../server/ServerAddressMaskPane.java | 108 --- .../server/ServerNetworkLatencyPane.java | 100 --- .../ui/instances/server/ServerStatusPane.java | 304 ------- 8 files changed, 791 insertions(+), 960 deletions(-) rename HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/{server => }/ServerListPage.java (51%) delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index 0b08cd8da69..27287e9c005 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -61,7 +61,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.JFXDialogPane; import org.jackhuang.hmcl.ui.construct.Navigator; -import org.jackhuang.hmcl.ui.instances.server.ServerStatusPane; +import org.jackhuang.hmcl.ui.instances.ServerListPage; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.WizardProvider; @@ -263,7 +263,7 @@ public Decorator(Node mainPage, Consumer stageInitializer) { // Setup Minecraft server DnD root.addEventFilter(DragEvent.DRAG_OVER, ServerDnD.dragOverHandler()); root.addEventFilter(DragEvent.DRAG_DROPPED, ServerDnD.dragDroppedHandler( - server -> Controllers.dialog(new ServerStatusPane(null, server, true, null)))); + server -> Controllers.dialog(new ServerListPage.ServerStatusDialog(null, server, true, null)))); } /// Returns the node that must be installed as the scene root. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 9a26f6feae3..377854748ec 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -43,7 +43,6 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.game.GameSettingsPage; -import org.jackhuang.hmcl.ui.instances.server.ServerListPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java similarity index 51% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java index f4d79e07ea2..4f202b69340 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ServerListPage.java @@ -15,41 +15,49 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.instances.server; +package org.jackhuang.hmcl.ui.instances; +import com.google.gson.JsonPrimitive; import com.jfoenix.controls.*; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; import javafx.animation.PauseTransition; +import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.binding.Bindings; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.*; +import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; +import javafx.geometry.HPos; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.*; +import javafx.scene.effect.BoxBlur; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; -import javafx.scene.layout.HBox; -import javafx.scene.layout.Priority; -import javafx.scene.layout.StackPane; -import javafx.scene.layout.VBox; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import javafx.scene.shape.SVGPath; import javafx.util.Duration; import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.server.Server; import org.jackhuang.hmcl.server.ServerStatus; import org.jackhuang.hmcl.server.ServerStatusResult; +import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; import org.jackhuang.hmcl.server.storage.DatFileModifiedExternalException; import org.jackhuang.hmcl.server.storage.ServerStorage; +import org.jackhuang.hmcl.setting.GameDirectoryManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.*; 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.instances.Instances; +import org.jackhuang.hmcl.util.MinecraftChatComponentUtils; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.javafx.Subscription; import org.jetbrains.annotations.NotNull; @@ -59,6 +67,8 @@ import java.io.IOException; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; @@ -125,7 +135,7 @@ public void showServerStatus(ServerListItem item) { public void editServer(ServerListItem item) { if (gameInstance != null) { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.EDIT, item.getServer(), (server) -> { + runInFX(() -> Controllers.dialog(new EditServerDialog(EditServerDialog.Type.EDIT, item.getServer(), (server) -> { if (!server.equals(item.getServer())) { Task.supplyAsync(Schedulers.io(), () -> { ServerStorage storage = item.storageEntry.rootStorage(); @@ -302,7 +312,7 @@ private void refresh() { } private void addServer() { - runInFX(() -> Controllers.dialog(new EditServerPane(EditServerPane.Type.ADD, null, this::addServer))); + runInFX(() -> Controllers.dialog(new EditServerDialog(EditServerDialog.Type.ADD, null, this::addServer))); } private void updateServerList() { @@ -322,29 +332,77 @@ private void updateServerList() { getItems().setAll(stream.toList()); } - public final class ServerListItem { - private final @NotNull ServerStorage.ServerStorageEntry storageEntry; - private final Image serverFaviconImage; + private static class ServerNetworkLatencyPane extends StackPane { + private static final SVG[] FRAMES = { + SVG.SERVER_SIGNAL_0_BAR, + SVG.SERVER_SIGNAL_1_BAR, + SVG.SERVER_SIGNAL_2_BAR, + SVG.SERVER_SIGNAL_3_BAR, + SVG.SERVER_SIGNAL_FULL, + SVG.SERVER_SIGNAL_3_BAR, + SVG.SERVER_SIGNAL_2_BAR, + SVG.SERVER_SIGNAL_1_BAR + }; + + private final SVGPath icon = SVG.NONE.createIcon(); + private final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(125), e -> nextFrame())); + private int index = 0; + private State state = State.PINGING; + + public ServerNetworkLatencyPane() { + timeline.setCycleCount(Animation.INDEFINITE); + getChildren().setAll(icon); + sceneProperty().addListener((obs, o, n) -> updateTimeline()); + ping(); + } - final @NotNull ObservableServerStatus observableServerStatus; + private void nextFrame() { + setIcon(FRAMES[index++ % FRAMES.length], "", 0.3); + } - public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry) { - this(storageEntry, new ObservableServerStatus(storageEntry.getServer().ip())); + private void updateTimeline() { + if (state == State.PINGING && getScene() != null) { + timeline.play(); + } else { + timeline.stop(); + } } - public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { - this.storageEntry = storageEntry; - this.observableServerStatus = observableServerStatus; + public void ping() { + state = State.PINGING; + index = 0; + nextFrame(); + updateTimeline(); + } - this.serverFaviconImage = parseServerFaviconImageOrDefault(storageEntry.getServer().icon()); + public void pong(long latency) { + state = State.PONG; + updateTimeline(); + + SVG svg = latency < 150 ? SVG.SERVER_SIGNAL_FULL + : latency < 300 ? SVG.SERVER_SIGNAL_3_BAR + : latency < 600 ? SVG.SERVER_SIGNAL_2_BAR + : latency < 1000 ? SVG.SERVER_SIGNAL_1_BAR + : SVG.SERVER_SIGNAL_0_BAR; + setIcon(svg, "", 1); } - public Server getServer() { - return storageEntry.getServer(); + public void error() { + state = State.ERROR; + updateTimeline(); + setIcon(SVG.SERVER_SIGNAL_ERROR, "-fx-fill: -monet-error;", 1); } - public void openServerStatusPane() { - Controllers.dialog(new ServerStatusPane(observableServerStatus, getServer(), ServerListPage.this.gameInstance)); + private void setIcon(SVG svg, String style, double opacity) { + icon.setContent(svg.getPath()); + icon.setStyle(style); + icon.setOpacity(opacity); + } + + private enum State { + PINGING, + PONG, + ERROR; } } @@ -776,4 +834,711 @@ private void search() { } } } + + public static class ServerStatusDialog extends TransitionPane implements DialogAware { + private final Server server; + private final SpinnerPane refreshSpinner = new SpinnerPane(); + private final JFXDialogLayout rootLayout = new JFXDialogLayout(); + private final Label lblErrorMessage = new Label(); + private final ObservableServerStatus observableServerStatus; + private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); + private final @Nullable HMCLGameInstance currentGameInstance; + + public ServerStatusDialog(ObservableServerStatus reference, Server server, @Nullable HMCLGameInstance gameInstance) { + this(reference, server, false, gameInstance); + } + + public ServerStatusDialog(ObservableServerStatus reference, Server server, boolean fromDnD, @Nullable HMCLGameInstance gameInstance) { + this.server = server; + if (gameInstance == null) { + this.currentGameInstance = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); + } else { + this.currentGameInstance = gameInstance; + } + this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(server.ip())); + + getStyleClass().add("skin-pane"); + getChildren().setAll(rootLayout); + rootLayout.setHeading(new Label(i18n("server.manage.status.head"))); + + { + JFXButton refreshBtn = new JFXButton(i18n("button.refresh")); + refreshBtn.getStyleClass().add("dialog-refresh"); + refreshBtn.setOnAction(e -> observableServerStatus.refreshAsync(true)); + refreshSpinner.getStyleClass().add("small-spinner-pane"); + refreshSpinner.setContent(refreshBtn); + } + + HBox actions = new HBox(refreshSpinner); + actions.setAlignment(Pos.CENTER_RIGHT); + + if (fromDnD) { + // add to current instance + + JFXButton addToInstallBtn = new JFXButton(i18n("server.dnd.add")); + addToInstallBtn.getStyleClass().add("dialog-accept"); + addToInstallBtn.setOnAction(ignored0 -> { + fireEvent(new DialogCloseEvent()); + // open server manager + if (currentGameInstance == null) { + JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download")); + gotoDownload.getStyleClass().add("dialog-accept"); + gotoDownload.setOnAction(ignored1 -> Controllers.navigate(Controllers.getDownloadPage())); + + Controllers.confirmAction(i18n("server.dnd.add.instanceempty.desc"), i18n("server.dnd.add.instanceempty"), + MessageDialogPane.MessageType.ERROR, + gotoDownload, + null); + } else { + if (Controllers.getGameInstancePage().getScene() == null) { + Instances.modifyServerList(currentGameInstance); + } + + if (!(Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage)) { + Controllers.getGameInstancePage().showServerList(); + } + + ServerListPage serverListPage = Controllers.getGameInstancePage().getServerListTab().getNode(); + if (serverListPage != null) { + serverListPage.addServer(server, currentGameInstance); + } + } + }); + actions.getChildren().add(addToInstallBtn); + } + + JFXButton launchBtn = new JFXButton(i18n("instance.launch")); + launchBtn.getStyleClass().add("dialog-accept"); + launchBtn.setOnAction(e -> { + fireEvent(new DialogCloseEvent()); + Instances.launchAndEnterServer(currentGameInstance, server.ip()); + }); + actions.getChildren().add(launchBtn); + + JFXButton cancelBtn = new JFXButton(i18n("button.cancel")); + cancelBtn.getStyleClass().add("dialog-cancel"); + cancelBtn.setOnAction(e -> onClose()); + onEscPressed(this, cancelBtn::fire); + actions.getChildren().add(cancelBtn); + + lblErrorMessage.setWrapText(true); + lblErrorMessage.setMaxWidth(400); + rootLayout.getActions().addAll(lblErrorMessage, actions); + + applyResult(observableServerStatus.resultProperty().get()); + listenerHolder.add(onWeakChangeAndOperate(observableServerStatus.resultProperty(), this::applyResult)); + + applyPinging(observableServerStatus.pingingProperty().get()); + listenerHolder.add(onWeakChangeAndOperate(observableServerStatus.pingingProperty(), this::applyPinging)); + + observableServerStatus.refreshIfNoResultOrFailedAsync(true); + } + + private static Node createDescriptionRow(String title, String content, @Nullable String tooltip, Insets inVBoxInsets) { + BorderPane row = new BorderPane(); + VBox.setMargin(row, inVBoxInsets); + row.setLeft(new Label(title)); + Label right = new Label(content); + if (tooltip != null) { + installFastTooltip(right, tooltip); + } + row.setRight(right); + return row; + } + + public void applyPinging(boolean pinging) { + if (pinging) { + lblErrorMessage.setText(""); + refreshSpinner.showSpinner(); + } else { + refreshSpinner.hideSpinner(); + } + } + + public void applyResult(ServerStatusResult statusResult) { + ServerStatus status; + + if (statusResult == null) { + // pinging..., continue + status = null; + } else { + // ping failed + status = statusResult.getIfSucceed(); + if (status == null) { + ServerStatusResult.FailureResult.Reason reason = statusResult.getFailureReasonIfFailed(); + lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { + case EXCEPTION -> i18n("server.manage.status.error"); + case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); + case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); + }); + } + } + + Image serverIcon = parseServerFaviconImageOrDefault(server.icon()); + if (status != null) { + serverIcon = parseServerFaviconImageOrDefault(status.favicon()); + } + + VBox rootContent = new VBox(); + rootContent.setSpacing(10); + rootLayout.setBody(rootContent); + + HBox titleContainer = new HBox(); + titleContainer.setSpacing(8); + titleContainer.setAlignment(Pos.CENTER_LEFT); + VBox.setMargin(titleContainer, new Insets(12, 0, 0, 0)); + rootContent.getChildren().add(titleContainer); + + ImageContainer imageView = new ImageContainer(64); + imageView.setImage(serverIcon); + titleContainer.getChildren().add(imageView); + + VBox titleContent = new VBox(); + titleContent.setAlignment(Pos.CENTER_LEFT); + titleContent.setSpacing(8); + HBox.setMargin(titleContent, new Insets(0, 0, 0, 12)); + titleContainer.getChildren().add(titleContent); + + TwoLineListItem title = new TwoLineListItem(); + title.getTitleLabel().setWrapText(true); + title.setTitle(server.name()); + titleContent.getChildren().add(title); + + ServerAddressMaskPane addressMaskPane = new ServerAddressMaskPane(server.ip()); + addressMaskPane.labelAddStyleClass("subtitle"); + titleContent.getChildren().add(addressMaskPane); + + if (status != null) { + VBox motdContentBox = new VBox(); + motdContentBox.setSpacing(5); + motdContentBox.setAlignment(Pos.CENTER_LEFT); + + VBox.setMargin(motdContentBox, new Insets(4, 0, 0, 4)); + rootContent.getChildren().add(motdContentBox); + + String[] motdLines = MinecraftChatComponentUtils + .toPlainStringFromChatComponent(status.description()) + .split("\\r?\\n"); + for (int i = 0; i < Math.min(motdLines.length, 2); i++) { + String line = motdLines[i].trim(); + if (line.length() > 80) { + line = line.substring(0, 77).trim() + "..."; + } + Label lineLabel = new Label(line); + lineLabel.setMaxWidth(560); + lineLabel.setWrapText(true); + motdContentBox.getChildren().add(lineLabel); + } + + Separator titleEndSeparator = new Separator(); + titleEndSeparator.setMaxWidth(Double.MAX_VALUE); + titleEndSeparator.setPadding(new Insets(4, 0, 0, 0)); + rootContent.getChildren().add(titleEndSeparator); + + String playersTooltip = null; + if (!status.players().samples().isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(String.join("\n", status.players().samples().stream() + .map(sample -> { + if (ServerStatus.Players.Sample.ANONYMOUS_PLAYER.equals(sample)) { + return i18n("server.anonymous_player"); + } + return MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(sample.name())); + }) + .toList()) + ); + + if (status.players().samples().size() < status.players().online()) { + sb.append("\n"); + sb.append(i18n("server.and_more_players", String.valueOf(status.players().online() - status.players().samples().size()))); + } + + playersTooltip = sb.toString(); + } + + String modsTooltip = null; + int showCount = 15; + if (status.modInfo() != null && !status.modInfo().modList().isEmpty()) { + StringBuilder sb = new StringBuilder(); + sb.append(String.join("\n", status.modInfo().modList().stream() + .limit(showCount) + .map(it -> it.modId() + " - " + it.version()) + .toList()) + ); + if (showCount < status.modInfo().modList().size()) { + sb.append("\n"); + sb.append(i18n("server.and_more_mods", String.valueOf(status.modInfo().modList().size() - showCount))); + } + + modsTooltip = sb.toString(); + } + + rootContent.getChildren().add(createDescriptionRow(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), modsTooltip, new Insets(0, 4, 0, 4))); + rootContent.getChildren().add(createDescriptionRow(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), playersTooltip, new Insets(0, 4, 0, 4))); + if (status.version() != null) { + rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); + } else { + rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), i18n("server.playversion.unknown"), null, new Insets(0, 4, 0, 4))); + } + rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", ((ServerStatusResult.SuccessResult) statusResult).getNetworkLatency()), null, new Insets(0, 4, 0, 4))); + } + + } + + private void onClose() { + fireEvent(new DialogCloseEvent()); + } + } + + public static class ServerAddressMaskPane extends HBox { + private final Label atLabel = new Label(); + private final Label realServerLabel = new Label(); + private final Label queryLabel = new Label(); + + private final Tooltip atLabelTooltip = new Tooltip(); + private final Tooltip queryLabelTooltip = new Tooltip(); + + public ServerAddressMaskPane(@Nullable String serverIP) { + getChildren().add(atLabel); + getChildren().add(realServerLabel); + getChildren().add(queryLabel); + + atLabel.setPickOnBounds(true); + queryLabel.setPickOnBounds(true); + + // FXUtils.installFastTooltip(atLabel, atLabelTooltip); + // FXUtils.installFastTooltip(queryLabel, queryLabelTooltip); + set(serverIP); + + BoxBlur blur = new BoxBlur(); + blur.setIterations(3); + + atLabel.setEffect(blur); + queryLabel.setEffect(blur); + + atLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { + atLabel.setEffect(null); + }); + atLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { + atLabel.setEffect(blur); + }); + + queryLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { + queryLabel.setEffect(null); + }); + queryLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { + queryLabel.setEffect(blur); + }); + } + + public void set(@Nullable String serverIP) { + if (serverIP == null) { + atLabel.setText(""); + realServerLabel.setText(""); + queryLabel.setText(""); + + atLabelTooltip.setText(""); + queryLabelTooltip.setText(""); + return; + } + int queryStart = serverIP.lastIndexOf('?'); + if (queryStart != -1) { + String queryStr = serverIP.substring(queryStart); + queryLabel.setText(queryStr); + atLabelTooltip.setText(queryStr); + serverIP = serverIP.substring(0, queryStart); + } else { + queryLabel.setText(""); + atLabelTooltip.setText(""); + } + + int atEnd = serverIP.indexOf('@'); + if (atEnd != -1) { + String atStr = serverIP.substring(0, atEnd + 1); + atLabel.setText(atStr); + atLabelTooltip.setText(atStr); + serverIP = serverIP.substring(atEnd + 1); + } else { + atLabel.setText(""); + atLabelTooltip.setText(""); + } + + realServerLabel.setText(serverIP); + } + + public void labelAddStyleClass(String styleClass) { + atLabel.getStyleClass().add(styleClass); + realServerLabel.getStyleClass().add(styleClass); + queryLabel.getStyleClass().add(styleClass); + } + } + + public static class ObservableServerStatus { + + private static final Semaphore PING_SEMAPHORE = new Semaphore(5); + + private final String serverIp; + private final ReadOnlyObjectWrapper resultProperty = new ReadOnlyObjectWrapper<>(); + private final ReadOnlyBooleanWrapper pingingProperty = new ReadOnlyBooleanWrapper(); + private final AtomicInteger refreshId = new AtomicInteger(); + + public ObservableServerStatus(String serverIp) { + this.serverIp = serverIp; + } + + public void refreshIfNoResultAsync(boolean urgent) { + if (resultProperty.get() == null) { + refreshAsync(urgent); + } + } + + public void refreshIfNoResultOrFailedAsync(boolean urgent) { + ServerStatusResult result = resultProperty.get(); + if (result == null || result.isFailure()) { + refreshAsync(urgent); + } + } + + public void refreshAsync(boolean urgent) { + Task.runAsync(Schedulers.javafx(), () -> { + + if (pingingProperty.get() && !urgent) { + return; + } + + int currentRefreshId = refreshId.incrementAndGet(); + pingingProperty.set(true); + + Task.supplyAsync(Schedulers.io(), () -> { + // expired. + if (currentRefreshId != refreshId.get()) return null; + + if (urgent) { + return ServerStatusPinger.getStatus(serverIp); + } + + PING_SEMAPHORE.acquireUninterruptibly(); + try { + // expired. + if (currentRefreshId != refreshId.get()) return null; + + return ServerStatusPinger.getStatus(serverIp); + } finally { + PING_SEMAPHORE.release(); + } + }).whenComplete(Schedulers.javafx(), (result, ignored) -> { + // expired. + if (currentRefreshId != refreshId.get()) return; + + try { + resultProperty.set(result); + } finally { + pingingProperty.set(false); + } + }).start(); + }).start(); + } + + public ReadOnlyBooleanProperty pingingProperty() { + return pingingProperty.getReadOnlyProperty(); + } + + public ReadOnlyObjectProperty resultProperty() { + return resultProperty.getReadOnlyProperty(); + } + } + + public static class EditServerDialog extends TransitionPane implements DialogAware { + private final Consumer handleCallback; + + private final Server reference; + private final Type type; + private final GridPane body = new GridPane(); + private final JFXTextField serverNameField = new JFXTextField(); + private final JFXTextField serverIpField = new JFXTextField(); + private final JFXComboBox serverPackStatusField = new JFXComboBox<>(); + + private final JFXButton btnAccept = new JFXButton(); + private final JFXButton btnCancel = new JFXButton(); + private final SpinnerPane spinner = new SpinnerPane(); + private final Label lblErrorLabel = new Label(); + + private final SimpleBooleanProperty stillActionProgress = new SimpleBooleanProperty(false); + + private boolean cancelled; + + public EditServerDialog(Type type, @Nullable Server reference, Consumer handleCallback) { + this.type = type; + this.reference = reference; + this.handleCallback = handleCallback; + this.cancelled = false; + + getStyleClass().add("skin-pane"); + + String referenceServerName; + String referenceServerIP; + Server.ServerPackStatus referenceServerPackStatus; + if (reference != null) { + referenceServerName = reference.name(); + referenceServerIP = reference.ip(); + referenceServerPackStatus = reference.serverPackStatus(); + } else { + referenceServerName = null; + referenceServerIP = null; + referenceServerPackStatus = null; + } + + JFXDialogLayout rootLayout = new JFXDialogLayout(); + getChildren().setAll(rootLayout); + + rootLayout.setHeading(new Label(switch (type) { + case EDIT -> i18n("server.manage.edit.head"); + case ADD -> i18n("server.manage.add.head"); + })); + + VBox bodyVbox = new VBox(); + bodyVbox.setSpacing(8); + + VBox.setMargin(body, new Insets(20, 0, 0, 0)); + body.setVgap(22); + body.setHgap(15); + body.setAlignment(Pos.CENTER); + + ColumnConstraints col0 = new ColumnConstraints(); + col0.setMinWidth(USE_PREF_SIZE); + body.getColumnConstraints().add(col0); + ColumnConstraints col1 = new ColumnConstraints(); + col1.setHgrow(Priority.ALWAYS); + body.getColumnConstraints().add(col1); + + + // server name + { + Label label = new Label(); + label.setText(i18n("server.name")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 0); + + serverNameField.setPromptText(i18n("server.name.def")); + serverNameField.setTextFormatter(new TextFormatter<>(change -> { + if (change.getControlNewText().length() <= 32) { + return change; + } + return null; + })); + body.add(serverNameField, 1, 0); + + if (referenceServerName != null) { + serverNameField.setText(referenceServerName); + } + } + + // server ip + { + Label label = new Label(); + label.setText(i18n("server.ip")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 1); + + serverIpField.setValidators( + new RequiredValidator(), + new MinecraftServerAddressValidator(i18n("server.address.malformed"), false) + ); + setValidateWhileTextChanged(serverIpField, true); + + body.add(serverIpField, 1, 1); + + if (referenceServerIP != null) { + serverIpField.setText(referenceServerIP); + } + + } + + // serverPackStatusField + { + Label label = new Label(); + label.setText(i18n("server.resourcepack")); + GridPane.setHalignment(label, HPos.LEFT); + body.add(label, 0, 2); + + serverPackStatusField.setConverter(stringConverter(status -> i18n("server.resourcepack." + status.name().toLowerCase(Locale.ROOT)))); + serverPackStatusField.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { + if (newVal == null && oldVal != null) { + serverPackStatusField.setValue(oldVal); + } + }); + serverPackStatusField.getItems().setAll(Server.ServerPackStatus.values()); + body.add(serverPackStatusField, 1, 2); + serverPackStatusField.setValue( + Objects.requireNonNullElse(referenceServerPackStatus, Server.ServerPackStatus.PROMPT) + ); + } + + bodyVbox.getChildren().add(body); + rootLayout.setBody(bodyVbox); + + btnAccept.setText(switch (type) { + case EDIT -> i18n("server.manage.edit.accept"); + case ADD -> i18n("server.manage.add.accept"); + }); + btnAccept.getStyleClass().add("dialog-accept"); + btnAccept.disableProperty().bind(new BooleanBinding() { + { + bind(serverIpField.textProperty()); + } + + @Override + protected boolean computeValue() { + return !serverIpField.validate(); + } + }); + + lblErrorLabel.setWrapText(true); + lblErrorLabel.setMaxWidth(400); + + btnCancel.setText(i18n("button.cancel")); + btnCancel.getStyleClass().add("dialog-cancel"); + onEscPressed(this, btnCancel::fire); + + spinner.getStyleClass().add("small-spinner-pane"); + spinner.setContent(btnAccept); + + HBox actions = new HBox(spinner, btnCancel); + actions.setAlignment(Pos.CENTER_RIGHT); + + rootLayout.setActions(lblErrorLabel, actions); + + btnCancel.setOnAction(e -> onCancel()); + btnAccept.setOnAction(ignored -> { + String serverName; + String serverIP = serverIpField.getText(); + Server.ServerPackStatus serverPackStatus = serverPackStatusField.getValue(); + if (serverIP == null) return; + if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { + serverName = i18n("server.name.def"); + } else { + serverName = serverNameField.getText(); + } + + if (!stillActionProgress.get()) { + onAdd(serverPackStatus, serverName, serverIP); + return; + } + + fireEvent(new DialogCloseEvent()); + if (reference != null) { + handleCallback.accept(reference + .withIpAndName(serverIP, serverName) + .withPackStatus(serverPackStatus) + ); + } else { + handleCallback.accept(new Server(serverPackStatus, false, null, serverIP, serverName)); + } + }); + + ChangeListener contentChangeListener = (ignored0, ignored1, ignored2) -> { + if (stillActionProgress.get()) { + hideStillAction(); + } + }; + // serverNameField.textProperty().addListener(contentChangeListener); + serverIpField.textProperty().addListener(contentChangeListener); + } + + private void showStillAction(String errorMessage) { + stillActionProgress.set(true); + lblErrorLabel.setText(errorMessage); + btnAccept.setText(switch (type) { + case EDIT -> i18n("server.manage.edit.still"); + case ADD -> i18n("server.manage.add.still"); + }); + } + + private void hideStillAction() { + stillActionProgress.set(false); + lblErrorLabel.setText(""); + btnAccept.setText(switch (type) { + case EDIT -> i18n("server.manage.edit.accept"); + case ADD -> i18n("server.manage.add.accept"); + }); + } + + private void onAdd(Server.ServerPackStatus packStatus, String serverName, String serverIP) { + body.setDisable(true); + spinner.showSpinner(); + + Task.supplyAsync(Schedulers.io(), () -> + ServerStatusPinger.getStatus(serverIP) + ).whenComplete(Schedulers.javafx(), (result, ignored) -> { + + ServerStatus status = result.getIfSucceed(); + if (status == null) { + spinner.hideSpinner(); + body.setDisable(false); + + ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); + showStillAction(switch (Objects.requireNonNull(reason)) { + case EXCEPTION -> i18n("server.manage.status.error"); + case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); + case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); + }); + } else { + if (getScene() == null || cancelled) { + return; + } + fireEvent(new DialogCloseEvent()); + if (reference != null) { + handleCallback.accept(reference + .withIpAndName(serverIP, serverName) + .withPackStatus(packStatus) + .withIcon(status.favicon()) + ); + } else { + handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); + } + } + }).start(); + } + + private void onCancel() { + cancelled = true; + fireEvent(new DialogCloseEvent()); + } + + @Override + public void onDialogShown() { + serverNameField.requestFocus(); + } + + public enum Type { + EDIT, + ADD + } + } + + public final class ServerListItem { + private final @NotNull ServerStorage.ServerStorageEntry storageEntry; + private final Image serverFaviconImage; + + final @NotNull ObservableServerStatus observableServerStatus; + + public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry) { + this(storageEntry, new ObservableServerStatus(storageEntry.getServer().ip())); + } + + public ServerListItem(@NotNull ServerStorage.ServerStorageEntry storageEntry, @NotNull ObservableServerStatus observableServerStatus) { + this.storageEntry = storageEntry; + this.observableServerStatus = observableServerStatus; + + this.serverFaviconImage = parseServerFaviconImageOrDefault(storageEntry.getServer().icon()); + } + + public Server getServer() { + return storageEntry.getServer(); + } + + public void openServerStatusPane() { + Controllers.dialog(new ServerStatusDialog(observableServerStatus, getServer(), ServerListPage.this.gameInstance)); + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java deleted file mode 100644 index 681093648dc..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/EditServerPane.java +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.instances.server; - -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXComboBox; -import com.jfoenix.controls.JFXDialogLayout; -import com.jfoenix.controls.JFXTextField; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.value.ChangeListener; -import javafx.geometry.HPos; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.control.Label; -import javafx.scene.control.TextFormatter; -import javafx.scene.layout.*; -import org.jackhuang.hmcl.server.Server; -import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; -import java.util.Objects; -import java.util.function.Consumer; - -import static org.jackhuang.hmcl.ui.FXUtils.*; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class EditServerPane extends TransitionPane implements DialogAware { - private final Consumer handleCallback; - - private final Server reference; - private final Type type; - private final GridPane body = new GridPane(); - private final JFXTextField serverNameField = new JFXTextField(); - private final JFXTextField serverIpField = new JFXTextField(); - private final JFXComboBox serverPackStatusField = new JFXComboBox<>(); - - private final JFXButton btnAccept = new JFXButton(); - private final JFXButton btnCancel = new JFXButton(); - private final SpinnerPane spinner = new SpinnerPane(); - private final Label lblErrorLabel = new Label(); - - private final SimpleBooleanProperty stillActionProgress = new SimpleBooleanProperty(false); - - private boolean cancelled; - - public EditServerPane(Type type, @Nullable Server reference, Consumer handleCallback) { - this.type = type; - this.reference = reference; - this.handleCallback = handleCallback; - this.cancelled = false; - - getStyleClass().add("skin-pane"); - - String referenceServerName; - String referenceServerIP; - Server.ServerPackStatus referenceServerPackStatus; - if (reference != null) { - referenceServerName = reference.name(); - referenceServerIP = reference.ip(); - referenceServerPackStatus = reference.serverPackStatus(); - } else { - referenceServerName = null; - referenceServerIP = null; - referenceServerPackStatus = null; - } - - JFXDialogLayout rootLayout = new JFXDialogLayout(); - getChildren().setAll(rootLayout); - - rootLayout.setHeading(new Label(switch (type) { - case EDIT -> i18n("server.manage.edit.head"); - case ADD -> i18n("server.manage.add.head"); - })); - - VBox bodyVbox = new VBox(); - bodyVbox.setSpacing(8); - - VBox.setMargin(body, new Insets(20, 0, 0, 0)); - body.setVgap(22); - body.setHgap(15); - body.setAlignment(Pos.CENTER); - - ColumnConstraints col0 = new ColumnConstraints(); - col0.setMinWidth(USE_PREF_SIZE); - body.getColumnConstraints().add(col0); - ColumnConstraints col1 = new ColumnConstraints(); - col1.setHgrow(Priority.ALWAYS); - body.getColumnConstraints().add(col1); - - - // server name - { - Label label = new Label(); - label.setText(i18n("server.name")); - GridPane.setHalignment(label, HPos.LEFT); - body.add(label, 0, 0); - - serverNameField.setPromptText(i18n("server.name.def")); - serverNameField.setTextFormatter(new TextFormatter<>(change -> { - if (change.getControlNewText().length() <= 32) { - return change; - } - return null; - })); - body.add(serverNameField, 1, 0); - - if (referenceServerName != null) { - serverNameField.setText(referenceServerName); - } - } - - // server ip - { - Label label = new Label(); - label.setText(i18n("server.ip")); - GridPane.setHalignment(label, HPos.LEFT); - body.add(label, 0, 1); - - serverIpField.setValidators( - new RequiredValidator(), - new MinecraftServerAddressValidator(i18n("server.address.malformed"), false) - ); - setValidateWhileTextChanged(serverIpField, true); - - body.add(serverIpField, 1, 1); - - if (referenceServerIP != null) { - serverIpField.setText(referenceServerIP); - } - - } - - // serverPackStatusField - { - Label label = new Label(); - label.setText(i18n("server.resourcepack")); - GridPane.setHalignment(label, HPos.LEFT); - body.add(label, 0, 2); - - serverPackStatusField.setConverter(stringConverter(status -> i18n("server.resourcepack." + status.name().toLowerCase(Locale.ROOT)))); - serverPackStatusField.getSelectionModel().selectedItemProperty().addListener((obs, oldVal, newVal) -> { - if (newVal == null && oldVal != null) { - serverPackStatusField.setValue(oldVal); - } - }); - serverPackStatusField.getItems().setAll(Server.ServerPackStatus.values()); - body.add(serverPackStatusField, 1, 2); - serverPackStatusField.setValue( - Objects.requireNonNullElse(referenceServerPackStatus, Server.ServerPackStatus.PROMPT) - ); - } - - bodyVbox.getChildren().add(body); - rootLayout.setBody(bodyVbox); - - btnAccept.setText(switch (type) { - case EDIT -> i18n("server.manage.edit.accept"); - case ADD -> i18n("server.manage.add.accept"); - }); - btnAccept.getStyleClass().add("dialog-accept"); - btnAccept.disableProperty().bind(new BooleanBinding() { - { - bind(serverIpField.textProperty()); - } - - @Override - protected boolean computeValue() { - return !serverIpField.validate(); - } - }); - - lblErrorLabel.setWrapText(true); - lblErrorLabel.setMaxWidth(400); - - btnCancel.setText(i18n("button.cancel")); - btnCancel.getStyleClass().add("dialog-cancel"); - onEscPressed(this, btnCancel::fire); - - spinner.getStyleClass().add("small-spinner-pane"); - spinner.setContent(btnAccept); - - HBox actions = new HBox(spinner, btnCancel); - actions.setAlignment(Pos.CENTER_RIGHT); - - rootLayout.setActions(lblErrorLabel, actions); - - btnCancel.setOnAction(e -> onCancel()); - btnAccept.setOnAction(ignored -> { - String serverName; - String serverIP = serverIpField.getText(); - Server.ServerPackStatus serverPackStatus = serverPackStatusField.getValue(); - if (serverIP == null) return; - if (serverNameField.getText() == null || serverNameField.getText().isEmpty()) { - serverName = i18n("server.name.def"); - } else { - serverName = serverNameField.getText(); - } - - if (!stillActionProgress.get()) { - onAdd(serverPackStatus, serverName, serverIP); - return; - } - - fireEvent(new DialogCloseEvent()); - if (reference != null) { - handleCallback.accept(reference - .withIpAndName(serverIP, serverName) - .withPackStatus(serverPackStatus) - ); - } else { - handleCallback.accept(new Server(serverPackStatus, false, null, serverIP, serverName)); - } - }); - - ChangeListener contentChangeListener = (ignored0, ignored1, ignored2) -> { - if (stillActionProgress.get()) { - hideStillAction(); - } - }; -// serverNameField.textProperty().addListener(contentChangeListener); - serverIpField.textProperty().addListener(contentChangeListener); - } - - private void showStillAction(String errorMessage) { - stillActionProgress.set(true); - lblErrorLabel.setText(errorMessage); - btnAccept.setText(switch (type) { - case EDIT -> i18n("server.manage.edit.still"); - case ADD -> i18n("server.manage.add.still"); - }); - } - - private void hideStillAction() { - stillActionProgress.set(false); - lblErrorLabel.setText(""); - btnAccept.setText(switch (type) { - case EDIT -> i18n("server.manage.edit.accept"); - case ADD -> i18n("server.manage.add.accept"); - }); - } - - private void onAdd(Server.ServerPackStatus packStatus, String serverName, String serverIP) { - body.setDisable(true); - spinner.showSpinner(); - - Task.supplyAsync(Schedulers.io(), () -> - ServerStatusPinger.getStatus(serverIP) - ).whenComplete(Schedulers.javafx(), (result, ignored) -> { - - ServerStatus status = result.getIfSucceed(); - if (status == null) { - spinner.hideSpinner(); - body.setDisable(false); - - ServerStatusResult.FailureResult.Reason reason = result.getFailureReasonIfFailed(); - showStillAction(switch (Objects.requireNonNull(reason)) { - case EXCEPTION -> i18n("server.manage.status.error"); - case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); - case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); - }); - } else { - if (getScene() == null || cancelled) { - return; - } - fireEvent(new DialogCloseEvent()); - if (reference != null) { - handleCallback.accept(reference - .withIpAndName(serverIP, serverName) - .withPackStatus(packStatus) - .withIcon(status.favicon()) - ); - } else { - handleCallback.accept(new Server(packStatus, false, status.favicon(), serverIP, serverName)); - } - } - }).start(); - } - - private void onCancel() { - cancelled = true; - fireEvent(new DialogCloseEvent()); - } - - public enum Type { - EDIT, - ADD - } - - @Override - public void onDialogShown() { - serverNameField.requestFocus(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java deleted file mode 100644 index 585d7a3d8ad..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ObservableServerStatus.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.instances.server; - -import javafx.beans.property.ReadOnlyBooleanProperty; -import javafx.beans.property.ReadOnlyBooleanWrapper; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.ReadOnlyObjectWrapper; -import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.server.pinger.ServerStatusPinger; -import org.jackhuang.hmcl.task.Schedulers; -import org.jackhuang.hmcl.task.Task; - -import java.util.concurrent.Semaphore; -import java.util.concurrent.atomic.AtomicInteger; - -public class ObservableServerStatus { - - private static final Semaphore PING_SEMAPHORE = new Semaphore(5); - - private final String serverIp; - private final ReadOnlyObjectWrapper resultProperty = new ReadOnlyObjectWrapper<>(); - private final ReadOnlyBooleanWrapper pingingProperty = new ReadOnlyBooleanWrapper(); - private final AtomicInteger refreshId = new AtomicInteger(); - - public ObservableServerStatus(String serverIp) { - this.serverIp = serverIp; - } - - public void refreshIfNoResultAsync(boolean urgent) { - if (resultProperty.get() == null) { - refreshAsync(urgent); - } - } - - public void refreshIfNoResultOrFailedAsync(boolean urgent) { - ServerStatusResult result = resultProperty.get(); - if (result == null || result.isFailure()) { - refreshAsync(urgent); - } - } - - public void refreshAsync(boolean urgent) { - Task.runAsync(Schedulers.javafx(), () -> { - - if (pingingProperty.get() && !urgent) { - return; - } - - int currentRefreshId = refreshId.incrementAndGet(); - pingingProperty.set(true); - - Task.supplyAsync(Schedulers.io(), () -> { - // expired. - if (currentRefreshId != refreshId.get()) return null; - - if (urgent) { - return ServerStatusPinger.getStatus(serverIp); - } - - PING_SEMAPHORE.acquireUninterruptibly(); - try { - // expired. - if (currentRefreshId != refreshId.get()) return null; - - return ServerStatusPinger.getStatus(serverIp); - } finally { - PING_SEMAPHORE.release(); - } - }).whenComplete(Schedulers.javafx(), (result, ignored) -> { - // expired. - if (currentRefreshId != refreshId.get()) return; - - try { - resultProperty.set(result); - } finally { - pingingProperty.set(false); - } - }).start(); - }).start(); - } - - public ReadOnlyBooleanProperty pingingProperty() { - return pingingProperty.getReadOnlyProperty(); - } - - public ReadOnlyObjectProperty resultProperty() { - return resultProperty.getReadOnlyProperty(); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java deleted file mode 100644 index 63c846e6181..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerAddressMaskPane.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.instances.server; - -import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; -import javafx.scene.effect.BoxBlur; -import javafx.scene.input.MouseEvent; -import javafx.scene.layout.HBox; -import org.jetbrains.annotations.Nullable; - -public class ServerAddressMaskPane extends HBox { - private final Label atLabel = new Label(); - private final Label realServerLabel = new Label(); - private final Label queryLabel = new Label(); - - private final Tooltip atLabelTooltip = new Tooltip(); - private final Tooltip queryLabelTooltip = new Tooltip(); - - public ServerAddressMaskPane(@Nullable String serverIP) { - getChildren().add(atLabel); - getChildren().add(realServerLabel); - getChildren().add(queryLabel); - - atLabel.setPickOnBounds(true); - queryLabel.setPickOnBounds(true); - -// FXUtils.installFastTooltip(atLabel, atLabelTooltip); -// FXUtils.installFastTooltip(queryLabel, queryLabelTooltip); - set(serverIP); - - BoxBlur blur = new BoxBlur(); - blur.setIterations(3); - - atLabel.setEffect(blur); - queryLabel.setEffect(blur); - - atLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { - atLabel.setEffect(null); - }); - atLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { - atLabel.setEffect(blur); - }); - - queryLabel.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { - queryLabel.setEffect(null); - }); - queryLabel.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> { - queryLabel.setEffect(blur); - }); - } - - public void set(@Nullable String serverIP) { - if (serverIP == null) { - atLabel.setText(""); - realServerLabel.setText(""); - queryLabel.setText(""); - - atLabelTooltip.setText(""); - queryLabelTooltip.setText(""); - return; - } - int queryStart = serverIP.lastIndexOf('?'); - if (queryStart != -1) { - String queryStr = serverIP.substring(queryStart); - queryLabel.setText(queryStr); - atLabelTooltip.setText(queryStr); - serverIP = serverIP.substring(0, queryStart); - } else { - queryLabel.setText(""); - atLabelTooltip.setText(""); - } - - int atEnd = serverIP.indexOf('@'); - if (atEnd != -1) { - String atStr = serverIP.substring(0, atEnd + 1); - atLabel.setText(atStr); - atLabelTooltip.setText(atStr); - serverIP = serverIP.substring(atEnd + 1); - } else { - atLabel.setText(""); - atLabelTooltip.setText(""); - } - - realServerLabel.setText(serverIP); - } - - public void labelAddStyleClass(String styleClass) { - atLabel.getStyleClass().add(styleClass); - realServerLabel.getStyleClass().add(styleClass); - queryLabel.getStyleClass().add(styleClass); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java deleted file mode 100644 index 37211a679da..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerNetworkLatencyPane.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.instances.server; - -import javafx.animation.Animation; -import javafx.animation.KeyFrame; -import javafx.animation.Timeline; -import javafx.scene.layout.StackPane; -import javafx.scene.shape.SVGPath; -import javafx.util.Duration; -import org.jackhuang.hmcl.ui.SVG; - -public class ServerNetworkLatencyPane extends StackPane { - private static final SVG[] FRAMES = { - SVG.SERVER_SIGNAL_0_BAR, - SVG.SERVER_SIGNAL_1_BAR, - SVG.SERVER_SIGNAL_2_BAR, - SVG.SERVER_SIGNAL_3_BAR, - SVG.SERVER_SIGNAL_FULL, - SVG.SERVER_SIGNAL_3_BAR, - SVG.SERVER_SIGNAL_2_BAR, - SVG.SERVER_SIGNAL_1_BAR - }; - - private final SVGPath icon = SVG.NONE.createIcon(); - private final Timeline timeline = new Timeline(new KeyFrame(Duration.millis(125), e -> nextFrame())); - private int index = 0; - private State state = State.PINGING; - - public ServerNetworkLatencyPane() { - timeline.setCycleCount(Animation.INDEFINITE); - getChildren().setAll(icon); - sceneProperty().addListener((obs, o, n) -> updateTimeline()); - ping(); - } - - private void nextFrame() { - setIcon(FRAMES[index++ % FRAMES.length], "", 0.3); - } - - private void updateTimeline() { - if (state == State.PINGING && getScene() != null) { - timeline.play(); - } else { - timeline.stop(); - } - } - - public void ping() { - state = State.PINGING; - index = 0; - nextFrame(); - updateTimeline(); - } - - public void pong(long latency) { - state = State.PONG; - updateTimeline(); - - SVG svg = latency < 150 ? SVG.SERVER_SIGNAL_FULL - : latency < 300 ? SVG.SERVER_SIGNAL_3_BAR - : latency < 600 ? SVG.SERVER_SIGNAL_2_BAR - : latency < 1000 ? SVG.SERVER_SIGNAL_1_BAR - : SVG.SERVER_SIGNAL_0_BAR; - setIcon(svg, "", 1); - } - - public void error() { - state = State.ERROR; - updateTimeline(); - setIcon(SVG.SERVER_SIGNAL_ERROR, "-fx-fill: -monet-error;", 1); - } - - private void setIcon(SVG svg, String style, double opacity) { - icon.setContent(svg.getPath()); - icon.setStyle(style); - icon.setOpacity(opacity); - } - - private enum State { - PINGING, - PONG, - ERROR; - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java deleted file mode 100644 index 87381772034..00000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/server/ServerStatusPane.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2026 huangyuhui 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 . - */ -package org.jackhuang.hmcl.ui.instances.server; - -import com.google.gson.JsonPrimitive; -import com.jfoenix.controls.JFXButton; -import com.jfoenix.controls.JFXDialogLayout; -import javafx.geometry.Insets; -import javafx.geometry.Pos; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.Separator; -import javafx.scene.image.Image; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.HBox; -import javafx.scene.layout.VBox; -import org.jackhuang.hmcl.game.HMCLGameInstance; -import org.jackhuang.hmcl.server.Server; -import org.jackhuang.hmcl.server.ServerStatus; -import org.jackhuang.hmcl.server.ServerStatusResult; -import org.jackhuang.hmcl.setting.GameDirectoryManager; -import org.jackhuang.hmcl.ui.Controllers; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.instances.Instances; -import org.jackhuang.hmcl.util.MinecraftChatComponentUtils; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; - -import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed; -import static org.jackhuang.hmcl.util.i18n.I18n.i18n; - -public class ServerStatusPane extends TransitionPane implements DialogAware { - private final Server server; - private final SpinnerPane refreshSpinner = new SpinnerPane(); - private final JFXDialogLayout rootLayout = new JFXDialogLayout(); - private final Label lblErrorMessage = new Label(); - private final ObservableServerStatus observableServerStatus; - private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private final @Nullable HMCLGameInstance currentGameInstance; - - public ServerStatusPane(ObservableServerStatus reference, Server server, @Nullable HMCLGameInstance gameInstance) { - this(reference, server, false, gameInstance); - } - - public ServerStatusPane(ObservableServerStatus reference, Server server, boolean fromDnD, @Nullable HMCLGameInstance gameInstance) { - this.server = server; - if (gameInstance == null) { - this.currentGameInstance = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); - } else { - this.currentGameInstance = gameInstance; - } - this.observableServerStatus = Objects.requireNonNullElseGet(reference, () -> new ObservableServerStatus(server.ip())); - - getStyleClass().add("skin-pane"); - getChildren().setAll(rootLayout); - rootLayout.setHeading(new Label(i18n("server.manage.status.head"))); - - { - JFXButton refreshBtn = new JFXButton(i18n("button.refresh")); - refreshBtn.getStyleClass().add("dialog-refresh"); - refreshBtn.setOnAction(e -> observableServerStatus.refreshAsync(true)); - refreshSpinner.getStyleClass().add("small-spinner-pane"); - refreshSpinner.setContent(refreshBtn); - } - - HBox actions = new HBox(refreshSpinner); - actions.setAlignment(Pos.CENTER_RIGHT); - - if (fromDnD) { - // add to current instance - - JFXButton addToInstallBtn = new JFXButton(i18n("server.dnd.add")); - addToInstallBtn.getStyleClass().add("dialog-accept"); - addToInstallBtn.setOnAction(ignored0 -> { - fireEvent(new DialogCloseEvent()); - // open server manager - if (currentGameInstance == null) { - JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download")); - gotoDownload.getStyleClass().add("dialog-accept"); - gotoDownload.setOnAction(ignored1 -> Controllers.navigate(Controllers.getDownloadPage())); - - Controllers.confirmAction(i18n("server.dnd.add.instanceempty.desc"), i18n("server.dnd.add.instanceempty"), - MessageDialogPane.MessageType.ERROR, - gotoDownload, - null); - } else { - if (Controllers.getGameInstancePage().getScene() == null) { - Instances.modifyServerList(currentGameInstance); - } - - if (!(Controllers.getGameInstancePage().getSelectedTab() instanceof ServerListPage)) { - Controllers.getGameInstancePage().showServerList(); - } - - ServerListPage serverListPage = Controllers.getGameInstancePage().getServerListTab().getNode(); - if (serverListPage != null) { - serverListPage.addServer(server, currentGameInstance); - } - } - }); - actions.getChildren().add(addToInstallBtn); - } - - JFXButton launchBtn = new JFXButton(i18n("instance.launch")); - launchBtn.getStyleClass().add("dialog-accept"); - launchBtn.setOnAction(e -> { - fireEvent(new DialogCloseEvent()); - Instances.launchAndEnterServer(currentGameInstance, server.ip()); - }); - actions.getChildren().add(launchBtn); - - JFXButton cancelBtn = new JFXButton(i18n("button.cancel")); - cancelBtn.getStyleClass().add("dialog-cancel"); - cancelBtn.setOnAction(e -> onClose()); - onEscPressed(this, cancelBtn::fire); - actions.getChildren().add(cancelBtn); - - lblErrorMessage.setWrapText(true); - lblErrorMessage.setMaxWidth(400); - rootLayout.getActions().addAll(lblErrorMessage, actions); - - applyResult(observableServerStatus.resultProperty().get()); - listenerHolder.add(FXUtils.onWeakChangeAndOperate(observableServerStatus.resultProperty(), this::applyResult)); - - applyPinging(observableServerStatus.pingingProperty().get()); - listenerHolder.add(FXUtils.onWeakChangeAndOperate(observableServerStatus.pingingProperty(), this::applyPinging)); - - observableServerStatus.refreshIfNoResultOrFailedAsync(true); - } - - public void applyPinging(boolean pinging) { - if (pinging) { - lblErrorMessage.setText(""); - refreshSpinner.showSpinner(); - } else { - refreshSpinner.hideSpinner(); - } - } - - private static Node createDescriptionRow(String title, String content, @Nullable String tooltip, Insets inVBoxInsets) { - BorderPane row = new BorderPane(); - VBox.setMargin(row, inVBoxInsets); - row.setLeft(new Label(title)); - Label right = new Label(content); - if (tooltip != null) { - FXUtils.installFastTooltip(right, tooltip); - } - row.setRight(right); - return row; - } - - public void applyResult(ServerStatusResult statusResult) { - ServerStatus status; - - if (statusResult == null) { - // pinging..., continue - status = null; - } else { - // ping failed - status = statusResult.getIfSucceed(); - if (status == null) { - ServerStatusResult.FailureResult.Reason reason = statusResult.getFailureReasonIfFailed(); - lblErrorMessage.setText(switch (Objects.requireNonNull(reason)) { - case EXCEPTION -> i18n("server.manage.status.error"); - case UNKNOWN_HOST -> i18n("server.manage.status.error.unknownhost"); - case BLOCKED_BY_MOJANG -> i18n("server.manage.status.error.blocked"); - }); - } - } - - Image serverIcon = ServerListPage.parseServerFaviconImageOrDefault(server.icon()); - if (status != null) { - serverIcon = ServerListPage.parseServerFaviconImageOrDefault(status.favicon()); - } - - VBox rootContent = new VBox(); - rootContent.setSpacing(10); - rootLayout.setBody(rootContent); - - HBox titleContainer = new HBox(); - titleContainer.setSpacing(8); - titleContainer.setAlignment(Pos.CENTER_LEFT); - VBox.setMargin(titleContainer, new Insets(12, 0, 0, 0)); - rootContent.getChildren().add(titleContainer); - - ImageContainer imageView = new ImageContainer(64); - imageView.setImage(serverIcon); - titleContainer.getChildren().add(imageView); - - VBox titleContent = new VBox(); - titleContent.setAlignment(Pos.CENTER_LEFT); - titleContent.setSpacing(8); - HBox.setMargin(titleContent, new Insets(0, 0, 0, 12)); - titleContainer.getChildren().add(titleContent); - - TwoLineListItem title = new TwoLineListItem(); - title.getTitleLabel().setWrapText(true); - title.setTitle(server.name()); - titleContent.getChildren().add(title); - - ServerAddressMaskPane addressMaskPane = new ServerAddressMaskPane(server.ip()); - addressMaskPane.labelAddStyleClass("subtitle"); - titleContent.getChildren().add(addressMaskPane); - - if (status != null) { - VBox motdContentBox = new VBox(); - motdContentBox.setSpacing(5); - motdContentBox.setAlignment(Pos.CENTER_LEFT); - - VBox.setMargin(motdContentBox, new Insets(4, 0, 0, 4)); - rootContent.getChildren().add(motdContentBox); - - String[] motdLines = MinecraftChatComponentUtils - .toPlainStringFromChatComponent(status.description()) - .split("\\r?\\n"); - for (int i = 0; i < Math.min(motdLines.length, 2); i++) { - String line = motdLines[i].trim(); - if (line.length() > 80) { - line = line.substring(0, 77).trim() + "..."; - } - Label lineLabel = new Label(line); - lineLabel.setMaxWidth(560); - lineLabel.setWrapText(true); - motdContentBox.getChildren().add(lineLabel); - } - - Separator titleEndSeparator = new Separator(); - titleEndSeparator.setMaxWidth(Double.MAX_VALUE); - titleEndSeparator.setPadding(new Insets(4, 0, 0, 0)); - rootContent.getChildren().add(titleEndSeparator); - - String playersTooltip = null; - if (!status.players().samples().isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append(String.join("\n", status.players().samples().stream() - .map(sample -> { - if (ServerStatus.Players.Sample.ANONYMOUS_PLAYER.equals(sample)) { - return i18n("server.anonymous_player"); - } - return MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(sample.name())); - }) - .toList()) - ); - - if (status.players().samples().size() < status.players().online()) { - sb.append("\n"); - sb.append(i18n("server.and_more_players", String.valueOf(status.players().online() - status.players().samples().size()))); - } - - playersTooltip = sb.toString(); - } - - String modsTooltip = null; - int showCount = 15; - if (status.modInfo() != null && !status.modInfo().modList().isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append(String.join("\n", status.modInfo().modList().stream() - .limit(showCount) - .map(it -> it.modId() + " - " + it.version()) - .toList()) - ); - if (showCount < status.modInfo().modList().size()) { - sb.append("\n"); - sb.append(i18n("server.and_more_mods", String.valueOf(status.modInfo().modList().size() - showCount))); - } - - modsTooltip = sb.toString(); - } - - rootContent.getChildren().add(createDescriptionRow(i18n("server.type"), status.modInfo() != null ? i18n("server.type.mod", status.modInfo().type(), status.modInfo().modList().size()) : i18n("server.type.vanilla"), modsTooltip, new Insets(0, 4, 0, 4))); - rootContent.getChildren().add(createDescriptionRow(i18n("server.onlineplayers"), String.format("%,d/%,d", status.players().online(), status.players().max()), playersTooltip, new Insets(0, 4, 0, 4))); - if (status.version() != null) { - rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), MinecraftChatComponentUtils.toPlainStringFromChatComponent(new JsonPrimitive(status.version().name())) + "(" + status.version().version() + ")", null, new Insets(0, 4, 0, 4))); - } else { - rootContent.getChildren().add(createDescriptionRow(i18n("server.playversion"), i18n("server.playversion.unknown"), null, new Insets(0, 4, 0, 4))); - } - rootContent.getChildren().add(createDescriptionRow(i18n("server.latency"), String.format("%,dms", ((ServerStatusResult.SuccessResult) statusResult).getNetworkLatency()), null, new Insets(0, 4, 0, 4))); - } - - } - - private void onClose() { - fireEvent(new DialogCloseEvent()); - } -}