feat: 默认高性能 GPU、首页 MOTD 公告、更新后重启 #23
No files matched your search
@@ -22,6 +22,7 @@ import org.jackhuang.hmcl.setting.JavaVersionType;
|
||||
import org.jackhuang.hmcl.setting.LauncherVisibility;
|
||||
import org.jackhuang.hmcl.task.*;
|
||||
import org.jackhuang.hmcl.ui.*;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
|
||||
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
|
||||
@@ -255,32 +256,12 @@ public final class LauncherHelper {
|
||||
}
|
||||
|
||||
if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && setting.getInheritable(GameSettings::highPerformanceProperty)) {
|
||||
@Nullable String javaPathGpuReg = FileUtils.getAbsolutePath(context.javaExecutable);
|
||||
try {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg != null) {
|
||||
Object current = reg.queryValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg
|
||||
);
|
||||
if (current == null) {
|
||||
boolean result = reg.setValue(
|
||||
WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences",
|
||||
javaPathGpuReg,
|
||||
"GpuPreference=2;"
|
||||
);
|
||||
if (result)
|
||||
LOG.info("Successfully applied high performance GPU preference for java: " + javaPathGpuReg);
|
||||
else
|
||||
LOG.warning("Failed to apply high performance GPU preference for java: " + javaPathGpuReg);
|
||||
} else {
|
||||
LOG.info("GPU preference for %s already exists: %s".formatted(javaPathGpuReg, current));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
||||
// Register both java.exe and javaw.exe: the game may be started through either one.
|
||||
applyHighPerformanceGpuPreference(context.javaExecutable);
|
||||
Path sibling = context.javaExecutable.resolveSibling(
|
||||
FileUtils.getName(context.javaExecutable).equalsIgnoreCase("javaw.exe") ? "java.exe" : "javaw.exe");
|
||||
if (Files.isRegularFile(sibling)) {
|
||||
applyHighPerformanceGpuPreference(sibling);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1048,11 +1029,44 @@ public final class LauncherHelper {
|
||||
runLater(() -> new GameCrashWindow(process, exitType, gameInstance, launchOptions, logs).show());
|
||||
}
|
||||
|
||||
if (exitType == ExitType.NORMAL) {
|
||||
// The game asked to be restarted (hot update): offer to update and relaunch.
|
||||
String restartReason = RestartRequest.consume(gameInstance.getRunDirectory());
|
||||
if (restartReason != null) {
|
||||
LOG.info("Game requested a restart, reason: " + restartReason);
|
||||
runLater(() -> Instances.offerRestart(gameInstance, restartReason));
|
||||
}
|
||||
}
|
||||
|
||||
checkExit();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Asks Windows to run `executable` on the high-performance GPU (`GpuPreference=2`) unless the user
|
||||
/// already has a preference stored for it.
|
||||
private static void applyHighPerformanceGpuPreference(Path executable) {
|
||||
String path = FileUtils.getAbsolutePath(executable);
|
||||
try {
|
||||
WinReg reg = WinReg.INSTANCE;
|
||||
if (reg == null) {
|
||||
return;
|
||||
}
|
||||
Object current = reg.queryValue(WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences", path);
|
||||
if (current != null) {
|
||||
LOG.info("GPU preference for %s already exists: %s".formatted(path, current));
|
||||
} else if (reg.setValue(WinReg.HKEY.HKEY_CURRENT_USER,
|
||||
"Software\\Microsoft\\DirectX\\UserGpuPreferences", path, "GpuPreference=2;")) {
|
||||
LOG.info("Successfully applied high performance GPU preference for java: " + path);
|
||||
} else {
|
||||
LOG.warning("Failed to apply high performance GPU preference for java: " + path);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOG.warning("Failed to apply high performance GPU preference", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Queue<WeakReference<ManagedProcess>> PROCESSES = new ConcurrentLinkedQueue<>();
|
||||
|
||||
public static int countMangedProcesses() {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/// The "please restart me" handshake between the game and the launcher.
|
||||
///
|
||||
/// When the game wants to be restarted (for example after a hot update), it writes
|
||||
/// [#FILE_NAME] into its run directory as `{"reason":"update"}` and exits normally. After the
|
||||
/// process ends the launcher calls [#consume(Path)], deletes the file and offers to update and relaunch.
|
||||
@NotNullByDefault
|
||||
public final class RestartRequest {
|
||||
private RestartRequest() {
|
||||
}
|
||||
|
||||
/// The marker file name inside the game's run directory.
|
||||
public static final String FILE_NAME = "tdlm-restart-request.json";
|
||||
|
||||
/// Reads and deletes the restart request in `runDirectory`.
|
||||
///
|
||||
/// A malformed file still counts as a request with reason `"unknown"`, because the file's presence is
|
||||
/// the signal. The file is always removed so a request cannot trigger twice.
|
||||
///
|
||||
/// @param runDirectory the game's run directory
|
||||
/// @return the reason, or `null` if no request was pending
|
||||
public static @Nullable String consume(Path runDirectory) {
|
||||
Path file = runDirectory.resolve(FILE_NAME);
|
||||
if (!Files.isRegularFile(file)) {
|
||||
return null;
|
||||
}
|
||||
String reason = "unknown";
|
||||
try {
|
||||
JsonObject object = JsonUtils.GSON.fromJson(Files.readString(file, StandardCharsets.UTF_8), JsonObject.class);
|
||||
if (object != null && object.has("reason") && object.get("reason").isJsonPrimitive()) {
|
||||
reason = object.get("reason").getAsString();
|
||||
}
|
||||
} catch (IOException | RuntimeException ignored) {
|
||||
// The presence of the file is the signal; keep the default reason.
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(file);
|
||||
} catch (IOException ignored) {
|
||||
// Nothing sensible to do; the worst case is one more prompt on the next exit.
|
||||
}
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
@@ -645,9 +645,12 @@ public sealed abstract class GameSettings extends ObservableSetting {
|
||||
public static final String PROPERTY_HIGH_PERFORMANCE = "highPerformance";
|
||||
|
||||
/// Whether to launch games with high-performance GPU.
|
||||
///
|
||||
/// On by default: on a laptop with an integrated and a discrete GPU, Windows otherwise picks the
|
||||
/// integrated one for java. Machines with a single GPU are unaffected by the preference.
|
||||
@SerializedName(PROPERTY_HIGH_PERFORMANCE)
|
||||
private final InheritableProperty<Boolean> highPerformance =
|
||||
newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, false);
|
||||
newInheritableProperty(PROPERTY_HIGH_PERFORMANCE, true);
|
||||
|
||||
/// Returns the high performance GPU preference property.
|
||||
public InheritableProperty<Boolean> highPerformanceProperty() {
|
||||
|
||||
@@ -398,6 +398,21 @@ public final class Instances {
|
||||
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false)));
|
||||
}
|
||||
|
||||
/// Asks whether to update and relaunch after the game requested a restart (see [RestartRequest]).
|
||||
///
|
||||
/// Yes runs the Start flow, which syncs the client pack and joins the server again; No leaves the
|
||||
/// launcher as it is. The request file is already deleted, so the prompt shows only once.
|
||||
///
|
||||
/// @param gameInstance the instance that just exited
|
||||
/// @param reason the reason from the request file, for example `update`
|
||||
public static void offerRestart(HMCLGameInstance gameInstance, String reason) {
|
||||
Controllers.confirm(
|
||||
i18n("home.restart." + ("update".equals(reason) ? "update" : "generic")),
|
||||
i18n("home.restart.title"),
|
||||
() -> updateInstance(gameInstance),
|
||||
null);
|
||||
}
|
||||
|
||||
/// Updates via fileApi through the Start flow — no install wizard.
|
||||
public static void updateInstance(HMCLGameInstance gameInstance) {
|
||||
gameInstance.getRepository().setSelectedInstance(gameInstance);
|
||||
|
||||
@@ -136,7 +136,19 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
card.getStyleClass().add("home-card");
|
||||
card.setAlignment(Pos.CENTER_LEFT);
|
||||
card.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
StackPane.setAlignment(card, Pos.BOTTOM_CENTER);
|
||||
// Server announcement (the server's MOTD), shown above the card only when there is one.
|
||||
Label motdLabel = new Label();
|
||||
motdLabel.getStyleClass().add("home-subtitle");
|
||||
motdLabel.setWrapText(true);
|
||||
motdLabel.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
Label motdTitle = factLabel(i18n("home.motd"));
|
||||
VBox motdBox = new VBox(3, motdTitle, motdLabel);
|
||||
motdBox.setPadding(new Insets(0, 0, 14, 4));
|
||||
motdBox.setVisible(false);
|
||||
motdBox.setManaged(false);
|
||||
VBox bottom = new VBox(motdBox, card);
|
||||
bottom.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
StackPane.setAlignment(bottom, Pos.BOTTOM_CENTER);
|
||||
|
||||
// Start always installs/updates from fileApi then joins — never opens install or instance UI.
|
||||
FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch);
|
||||
@@ -157,7 +169,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
routeFact.setVisible(route != null);
|
||||
routeFact.setManaged(route != null);
|
||||
routeValue.setText(route != null ? route.name() : "");
|
||||
refreshServerStatus(route, serverFact, serverValue);
|
||||
refreshServerStatus(route, serverFact, serverValue, motdBox, motdLabel);
|
||||
|
||||
// With more than one route (for example direct and CDN), the route can be switched.
|
||||
List<ServerEndpoint.Route> routes = ServerEndpoint.routes(currentGame.getModpackConfigurationFile());
|
||||
@@ -174,7 +186,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
items.add(new IconedMenuItem(null, candidate.name(), () -> {
|
||||
ServerEndpoint.select(currentGame.getModpackConfigurationFile(), candidate);
|
||||
routeValue.setText(candidate.name() + " ▾");
|
||||
refreshServerStatus(candidate, serverFact, serverValue);
|
||||
refreshServerStatus(candidate, serverFact, serverValue, motdBox, motdLabel);
|
||||
}, popup));
|
||||
}
|
||||
list.getContent().setAll(items);
|
||||
@@ -183,7 +195,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
}
|
||||
});
|
||||
|
||||
getChildren().addAll(art, hero, card);
|
||||
getChildren().addAll(art, hero, bottom);
|
||||
}
|
||||
|
||||
/// Sets the home button to install / update / join based on the selected instance and remote manifest.
|
||||
@@ -208,26 +220,39 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
/// Pings the server behind `route` in the background and shows the result in `serverValue`.
|
||||
///
|
||||
/// WebSocket routes cannot be pinged with plain TCP, so the status is hidden for them.
|
||||
private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue) {
|
||||
private static void refreshServerStatus(ServerEndpoint.@Nullable Route route, Node serverFact, Label serverValue,
|
||||
Node motdBox, Label motdLabel) {
|
||||
String address = route == null ? "" : route.address().trim();
|
||||
boolean pingable = !address.isEmpty() && !address.toLowerCase(java.util.Locale.ROOT).startsWith("ws");
|
||||
serverFact.setVisible(pingable);
|
||||
serverFact.setManaged(pingable);
|
||||
motdBox.setVisible(false);
|
||||
motdBox.setManaged(false);
|
||||
if (!pingable) {
|
||||
return;
|
||||
}
|
||||
serverValue.setText("…");
|
||||
Schedulers.io().execute(() -> {
|
||||
String text;
|
||||
String motd = "";
|
||||
try {
|
||||
ServerAddress parsed = ServerAddress.parse(address);
|
||||
ServerStatusPing.Status status = ServerStatusPing.ping(parsed.host(), parsed.port() >= 0 ? parsed.port() : 25565, 3000);
|
||||
text = status.online() + " / " + status.max() + " · " + status.latencyMillis() + " ms";
|
||||
motd = status.motd();
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
text = i18n("home.server.unreachable");
|
||||
}
|
||||
String result = text;
|
||||
FXUtils.runInFX(() -> serverValue.setText(result));
|
||||
String announcement = motd;
|
||||
FXUtils.runInFX(() -> {
|
||||
serverValue.setText(result);
|
||||
// Unreachable or empty MOTD: stay silent, the announcement area stays hidden.
|
||||
boolean show = !announcement.isEmpty();
|
||||
motdLabel.setText(announcement);
|
||||
motdBox.setVisible(show);
|
||||
motdBox.setManaged(show);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -808,6 +808,10 @@ first_run.microsoft=Sign in with Microsoft
|
||||
first_run.invalid=Use 1 to 16 letters, digits or underscores.
|
||||
home.server=Server
|
||||
home.server.unreachable=Unreachable
|
||||
home.motd=Server announcement
|
||||
home.restart.title=Restart game
|
||||
home.restart.update=The game closed to apply an update. Update the client and start it again now?
|
||||
home.restart.generic=The game asked to be restarted. Update the client and start it again now?
|
||||
server.join=Join Server
|
||||
server.update=Update
|
||||
instance.launch_and_enter_world=Play World
|
||||
|
||||
@@ -628,6 +628,10 @@ first_run.microsoft=登正版
|
||||
first_run.invalid=一至十六字,限英文數字底綫。
|
||||
home.server=服
|
||||
home.server.unreachable=不通
|
||||
home.motd=服公告
|
||||
home.restart.title=重啟遊戲
|
||||
home.restart.update=遊戲爲更新而止。今更新並重啟乎?
|
||||
home.restart.generic=遊戲請重啟。今更新並重啟乎?
|
||||
server.join=入服
|
||||
server.update=更新
|
||||
# instance.launch_and_enter_world=
|
||||
|
||||
@@ -631,6 +631,10 @@ first_run.microsoft=登录正版账号
|
||||
first_run.invalid=只能用 1 到 16 个英文字母、数字或下划线。
|
||||
home.server=服务器
|
||||
home.server.unreachable=连接不上
|
||||
home.motd=服务器公告
|
||||
home.restart.title=重启游戏
|
||||
home.restart.update=游戏为应用更新已退出。现在更新客户端并重新启动游戏吗?
|
||||
home.restart.generic=游戏请求重启。现在更新客户端并重新启动吗?
|
||||
server.join=进入服务器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=进入世界
|
||||
|
||||
@@ -625,6 +625,10 @@ first_run.microsoft=登入正版帳號
|
||||
first_run.invalid=只能使用 1 到 16 個英文字母、數字或底線。
|
||||
home.server=伺服器
|
||||
home.server.unreachable=無法連線
|
||||
home.motd=伺服器公告
|
||||
home.restart.title=重啟遊戲
|
||||
home.restart.update=遊戲為套用更新已結束。現在更新客戶端並重新啟動遊戲嗎?
|
||||
home.restart.generic=遊戲要求重啟。現在更新客戶端並重新啟動嗎?
|
||||
server.join=進入伺服器
|
||||
server.update=更新
|
||||
instance.launch_and_enter_world=進入世界
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/// Tests [RestartRequest].
|
||||
public final class RestartRequestTest {
|
||||
@Test
|
||||
void noFileMeansNoRequest(@TempDir Path dir) {
|
||||
assertNull(RestartRequest.consume(dir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readsReasonAndDeletesFile(@TempDir Path dir) throws Exception {
|
||||
Path file = dir.resolve(RestartRequest.FILE_NAME);
|
||||
Files.writeString(file, "{\"reason\":\"update\"}");
|
||||
assertEquals("update", RestartRequest.consume(dir));
|
||||
assertFalse(Files.exists(file));
|
||||
assertNull(RestartRequest.consume(dir));
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedFileStillCounts(@TempDir Path dir) throws Exception {
|
||||
Path file = dir.resolve(RestartRequest.FILE_NAME);
|
||||
Files.writeString(file, "not json");
|
||||
assertEquals("unknown", RestartRequest.consume(dir));
|
||||
assertFalse(Files.exists(file));
|
||||
}
|
||||
}
|
||||
@@ -799,6 +799,10 @@ public class DefaultLauncher extends Launcher {
|
||||
env.put("__NV_PRIME_RENDER_OFFLOAD", "1");
|
||||
env.put("__GLX_VENDOR_LIBRARY_NAME", "nvidia");
|
||||
env.put("__VK_LAYER_NV_optimus", "NVIDIA_only");
|
||||
} else if (HardwareVendor.AMD.equals(discreteGraphicsCard.getVendor())
|
||||
|| HardwareVendor.NVIDIA.equals(discreteGraphicsCard.getVendor())) {
|
||||
// Mesa PRIME offload (open source AMD / nouveau drivers)
|
||||
env.put("DRI_PRIME", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.util;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParseException;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataInputStream;
|
||||
@@ -28,7 +30,40 @@ public final class ServerStatusPing {
|
||||
/// @param online the number of players online
|
||||
/// @param max the player limit
|
||||
/// @param latencyMillis the round-trip time of the ping packet in milliseconds
|
||||
public record Status(int online, int max, long latencyMillis) {
|
||||
/// @param motd the server's message of the day as plain text without colour codes; empty if absent
|
||||
public record Status(int online, int max, long latencyMillis, String motd) {
|
||||
/// Creates a status without a message of the day.
|
||||
public Status(int online, int max, long latencyMillis) {
|
||||
this(online, max, latencyMillis, "");
|
||||
}
|
||||
}
|
||||
|
||||
/// Flattens the `description` field of a status response (a plain string or a chat component with
|
||||
/// `text` and `extra`) into plain text, dropping `§x` colour and format codes.
|
||||
///
|
||||
/// @param description the `description` element, may be `null`
|
||||
/// @return the plain text, trimmed; empty when there is nothing to show
|
||||
public static String motdToPlainText(@Nullable JsonElement description) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendComponent(description, builder);
|
||||
return builder.toString().replaceAll("\u00a7.", "").strip();
|
||||
}
|
||||
|
||||
private static void appendComponent(@Nullable JsonElement element, StringBuilder out) {
|
||||
if (element == null || element.isJsonNull()) {
|
||||
return;
|
||||
}
|
||||
if (element.isJsonPrimitive()) {
|
||||
out.append(element.getAsString());
|
||||
} else if (element.isJsonArray()) {
|
||||
for (JsonElement item : element.getAsJsonArray()) {
|
||||
appendComponent(item, out);
|
||||
}
|
||||
} else if (element.isJsonObject()) {
|
||||
JsonObject object = element.getAsJsonObject();
|
||||
appendComponent(object.get("text"), out);
|
||||
appendComponent(object.get("extra"), out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The protocol number sent in the handshake; Minecraft 1.7.10 uses 5 and servers answer any value.
|
||||
@@ -73,9 +108,11 @@ public final class ServerStatusPing {
|
||||
in.readFully(json);
|
||||
int online;
|
||||
int max;
|
||||
String motd;
|
||||
try {
|
||||
JsonObject players = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class)
|
||||
.getAsJsonObject("players");
|
||||
JsonObject response = JsonUtils.GSON.fromJson(new String(json, StandardCharsets.UTF_8), JsonObject.class);
|
||||
motd = motdToPlainText(response.get("description"));
|
||||
JsonObject players = response.getAsJsonObject("players");
|
||||
online = players.get("online").getAsInt();
|
||||
max = players.get("max").getAsInt();
|
||||
} catch (JsonParseException | NullPointerException | ClassCastException | IllegalStateException e) {
|
||||
@@ -94,7 +131,7 @@ public final class ServerStatusPing {
|
||||
throw new IOException("Unexpected pong packet");
|
||||
}
|
||||
in.readLong();
|
||||
return new Status(online, max, (System.nanoTime() - start) / 1_000_000);
|
||||
return new Status(online, max, (System.nanoTime() - start) / 1_000_000, motd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ public final class ServerStatusPingTest {
|
||||
var status = ServerStatusPing.ping("127.0.0.1", server.getLocalPort(), 3000);
|
||||
assertEquals(12, status.online());
|
||||
assertEquals(50, status.max());
|
||||
assertEquals("TDLM 公告: 今晚维护", status.motd());
|
||||
assertTrue(status.latencyMillis() >= 0);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +46,7 @@ public final class ServerStatusPingTest {
|
||||
skipPacket(in); // handshake
|
||||
skipPacket(in); // status request
|
||||
|
||||
byte[] json = "{\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] json = "{\"description\":{\"text\":\"§aTDLM \",\"extra\":[{\"text\":\"公告: 今晚维护\"}]},\"players\":{\"online\":12,\"max\":50}}".getBytes(StandardCharsets.UTF_8);
|
||||
byte[] body = concat(new byte[]{0x00}, varInt(json.length), json);
|
||||
out.write(varInt(body.length));
|
||||
out.write(body);
|
||||
@@ -88,4 +89,11 @@ public final class ServerStatusPingTest {
|
||||
for (byte[] part : parts) out.writeBytes(part);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void flattensPlainStringMotd() {
|
||||
var json = com.google.gson.JsonParser.parseString("\"§l欢迎§r来玩\"");
|
||||
assertEquals("欢迎来玩", ServerStatusPing.motdToPlainText(json));
|
||||
assertEquals("", ServerStatusPing.motdToPlainText(null));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user