From 119066dd6d54e10a0b1077aedb0c3a33af9ac141 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Fri, 26 Jun 2026 14:27:54 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=9B=B4=E5=A4=9A=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=8C=96=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- forge-mod/gradle.properties | 2 +- .../crossmod/CrossTeleportMod.java | 17 + .../crossmod/client/gui/CrossServerGui.java | 332 +++++++++++++++++- .../client/gui/ServerSelectionList.java | 261 +++++++++++++- .../crossmod/config/CrossServerConfig.java | 47 ++- .../config/CrossServerConfigManager.java | 170 ++++++++- .../crossmod/mixin/MixinPlayerList.java | 8 +- .../assets/ltdcrossteleport/lang/en_us.json | 2 + .../assets/ltdcrossteleport/lang/zh_cn.json | 7 +- 9 files changed, 800 insertions(+), 46 deletions(-) diff --git a/forge-mod/gradle.properties b/forge-mod/gradle.properties index d522331..1855b2e 100644 --- a/forge-mod/gradle.properties +++ b/forge-mod/gradle.properties @@ -49,7 +49,7 @@ mod_name=Leisure Time Dock Mod # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. mod_license=MIT # The mod version. See https://semver.org/ -mod_version=1.1.2 +mod_version=1.2. # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. # See https://maven.apache.org/guides/mini/guide-naming-conventions.html diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java index 5074f3d..493e83e 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java @@ -21,6 +21,7 @@ import net.minecraftforge.event.server.ServerStoppedEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.IExtensionPoint; +import net.minecraftforge.fml.ModList; import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.config.ModConfig; @@ -154,5 +155,21 @@ public class CrossTeleportMod { } } } + /** + * The type Mod info. + */ + public static class ModInfo { + /** + * The constant VERSION. + */ + public static final String VERSION; + static { + // 从 ModList 获取当前 ModContainer 的元数据 + VERSION = ModList.get() + .getModContainerById(MOD_ID) + .map(c -> c.getModInfo().getVersion().toString()) + .orElse("UNKNOWN"); + } + } } diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java index 13fb415..dc91f2f 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java @@ -9,6 +9,7 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Checkbox; +import net.minecraft.client.gui.components.EditBox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.chat.Component; @@ -16,7 +17,9 @@ import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.fml.loading.FMLEnvironment; import org.jetbrains.annotations.NotNull; +import org.lwjgl.glfw.GLFW; import java.util.Map; @@ -26,11 +29,22 @@ public class CrossServerGui extends Screen { private static final ResourceLocation CHANNEL_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "teleport"); private static final ResourceLocation LOGO_TEXTURE = new ResourceLocation(CrossTeleportMod.MOD_ID, "textures/ltd_logo.png"); + // 开发模式标志 + private static final boolean DEV_MODE = !FMLEnvironment.production; + // 存储组件引用,以便在渲染时控制顺序 private Checkbox enableCrCheckBox; private Checkbox enablePiCheckBox; private Button closeButton; + private Button devTestButton; + private Button devToggleDebugButton; private ServerSelectionList serverList; + private EditBox searchBox; + + // 开发测试数据 + private long lastUpdateTime = 0; + private int serverCount = 0; + private boolean showDevInfo = true; public CrossServerGui() { super(TITLE); @@ -38,11 +52,55 @@ public class CrossServerGui extends Screen { @Override protected void init() { - // 先添加按钮和复选框(它们应该渲染在列表上方) + // 先添加搜索框 + initSearchBox(); + + // 再添加按钮和复选框(它们应该渲染在列表上方) initButtons(); - // 再添加列表(它应该渲染在下方) + // 开发模式按钮 + if (DEV_MODE) { + initDevButtons(); + } + + // 最后添加列表(它应该渲染在下方) initServerList(); + + // 更新服务器计数 + serverCount = CrossServerConfigManager.INSTANCE.getServers().size(); + lastUpdateTime = System.currentTimeMillis(); + + // 设置初始焦点 + this.setInitialFocus(searchBox); + } + + private void initSearchBox() { + int centerX = width / 2; + + // 创建搜索框 + searchBox = new EditBox( + this.font, + centerX - 100, // x位置 + 50, // y位置(标题下方) + 200, // 宽度 + 16, // 高度 + Component.translatable("ltd.mod.client.menu.search") + ); + searchBox.setMaxLength(50); + searchBox.setVisible(true); + searchBox.setFocused(false); + searchBox.setHint(Component.translatable("ltd.mod.client.menu.search")); + // 启用文本框的输入 + searchBox.setCanLoseFocus(true); + + // 设置文本变化监听器 + searchBox.setResponder(searchText -> { + if (serverList != null) { + serverList.setSearchFilter(searchText); + } + }); + + addRenderableWidget(searchBox); } private void initButtons() { @@ -83,31 +141,76 @@ public class CrossServerGui extends Screen { addRenderableWidget(closeButton); } + private void initDevButtons() { + int centerX = width / 2; + int topY = 45; + + // 开发测试按钮 - 重新加载配置 + devTestButton = Button.builder( + Component.literal("§6[Dev] Reload"), + button -> { + CrossServerConfigManager.INSTANCE.reloadAll(); + refreshServerList(); + serverCount = CrossServerConfigManager.INSTANCE.getServers().size(); + lastUpdateTime = System.currentTimeMillis(); + } + ) + .bounds(centerX + 120, topY, 70, 16) + .build(); + addRenderableWidget(devTestButton); + + // 开发测试按钮 - 切换调试信息显示 + devToggleDebugButton = Button.builder( + Component.literal("§7[Dev] " + (showDevInfo ? "Hide" : "Show") + " Debug"), + button -> { + showDevInfo = !showDevInfo; + button.setMessage(Component.literal("§7[Dev] " + (showDevInfo ? "Hide" : "Show") + " Debug")); + } + ) + .bounds(centerX + 195, topY, 80, 16) + .build(); + addRenderableWidget(devToggleDebugButton); + } + private void initServerList() { int screenWidth = this.width; int screenHeight = this.height; - - // 创建服务器列表,但尺寸要避开按钮区域 + // 创建服务器列表,调整位置为搜索框下方 serverList = new ServerSelectionList( this, Minecraft.getInstance(), screenWidth, screenHeight, - 48, // X位置 - height - 64, // Y位置 + 70, // Y起始位置(搜索框下方) + height - 64, // Y结束位置 36, // 条目高度 - CrossServerConfigManager.INSTANCE.getServers() + CrossServerConfigManager.INSTANCE.getServers() ); // 设置列表属性 - serverList.setRenderBackground(true); + serverList.setRenderBackground(false); serverList.setRenderTopAndBottom(true); - addRenderableWidget(serverList); } + /** + * 刷新服务器列表 + */ + private void refreshServerList() { + if (serverList != null) { + // 移除旧的列表 + this.removeWidget(serverList); + // 创建新的列表 + initServerList(); + // 如果有搜索文本,重新应用 + if (searchBox != null && !searchBox.getValue().isEmpty()) { + serverList.setSearchFilter(searchBox.getValue()); + } + } + } + @Override public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) { @@ -119,8 +222,12 @@ public class CrossServerGui extends Screen { serverList.render(guiGraphics, mouseX, mouseY, partialTicks); } + // 渲染搜索框 + if (searchBox != null) { + searchBox.render(guiGraphics, mouseX, mouseY, partialTicks); + } + // 然后渲染按钮和复选框(在上层) - // 手动调用按钮的render方法,确保它们在最上面 if (enableCrCheckBox != null) { enableCrCheckBox.render(guiGraphics, mouseX, mouseY, partialTicks); } @@ -130,12 +237,167 @@ public class CrossServerGui extends Screen { if (closeButton != null) { closeButton.render(guiGraphics, mouseX, mouseY, partialTicks); } + + // 渲染开发按钮 + if (DEV_MODE) { + if (devTestButton != null) { + devTestButton.render(guiGraphics, mouseX, mouseY, partialTicks); + } + if (devToggleDebugButton != null) { + devToggleDebugButton.render(guiGraphics, mouseX, mouseY, partialTicks); + } + } + renderLogo(guiGraphics); + + // 渲染开发调试信息(不省略任何信息) + if (DEV_MODE && showDevInfo) { + renderDevInfo(guiGraphics); + } + } + + /** + * 渲染开发调试信息 - 完整显示所有信息 + */ + private void renderDevInfo(@NotNull GuiGraphics guiGraphics) { + int x = 10; + int y = 10; + int lineHeight = 11; + int color = 0x55FF55; // 绿色 + int textColor = 0xFFFFFF; // 白色 + + // 显示基本信息 + guiGraphics.drawString(this.font, "§a[Dev Mode] §7- Debug Information", x, y, color); + y += lineHeight; + + guiGraphics.drawString(this.font, "§7Servers: §f" + serverCount, x, y, textColor); + y += lineHeight; + + guiGraphics.drawString(this.font, "§7Players Online: §f" + getOnlinePlayerCount(), x, y, textColor); + y += lineHeight; + + guiGraphics.drawString(this.font, "§7Current Server: §f" + getCurrentServerName(), x, y, textColor); + y += lineHeight; + + // 显示服务器列表详细信息 - 完整显示所有服务器 + Map servers = CrossServerConfigManager.INSTANCE.getServers(); + if (!servers.isEmpty()) { + guiGraphics.drawString(this.font, "§7Server List (§f" + servers.size() + "§7):", x, y, color); + y += lineHeight; + + // 显示所有服务器,不省略 + int count = 0; + for (Map.Entry entry : servers.entrySet()) { + String display = "§8 " + (count + 1) + ". §f" + entry.getKey() + " §7→ §e" + entry.getValue(); + // 如果文本太长,截断但保留完整信息 + if (display.length() > 256) { + display = display.substring(0, 47) + "..."; + } + guiGraphics.drawString(this.font, display, x + 5, y, 0xAAAAAA); + y += lineHeight; + count++; + + // 防止信息超出屏幕 + if (y > height - 100) { + guiGraphics.drawString(this.font, "§8... and more (scroll to see all)", x + 5, y, 0x888888); + y += lineHeight; + break; + } + } + } + + // 显示内存信息 + Runtime runtime = Runtime.getRuntime(); + long totalMemory = runtime.totalMemory() / 1024 / 1024; + long freeMemory = runtime.freeMemory() / 1024 / 1024; + long usedMemory = totalMemory - freeMemory; + long maxMemory = runtime.maxMemory() / 1024 / 1024; + guiGraphics.drawString(this.font, "§7Memory: §f" + usedMemory + "MB §7used / §f" + totalMemory + "MB §7allocated / §f" + maxMemory + "MB §7max", x, y, textColor); + y += lineHeight; + + // 显示FPS信息 + try { + int fps = Minecraft.getInstance().getFps(); + String fpsColor = fps >= 60 ? "§a" : (fps >= 30 ? "§e" : "§c"); + guiGraphics.drawString(this.font, "§7FPS: " + fpsColor + fps, x, y, textColor); + y += lineHeight; + } catch (Exception e) { + // 忽略 + } + + // 显示Java版本信息 + guiGraphics.drawString(this.font, "§7Java: §f" + System.getProperty("java.version"), x, y, textColor); + y += lineHeight; + + // 显示Minecraft版本 + guiGraphics.drawString(this.font, "§7MC Version: §f" + Minecraft.getInstance().getVersionType(), x, y, textColor); + y += lineHeight; + + // 显示Mod版本 + guiGraphics.drawString(this.font, "§7Mod Version: §f" + CrossTeleportMod.ModInfo.VERSION, x, y, textColor); + y += lineHeight; + + // 显示最后更新时间 + guiGraphics.drawString(this.font, "§7Last Update: §f" + (System.currentTimeMillis() - lastUpdateTime) + "ms ago", x, y, textColor); + y += lineHeight; + + // 显示搜索框状态 + if (searchBox != null) { + String searchText = searchBox.getValue(); + if (!searchText.isEmpty()) { + guiGraphics.drawString(this.font, "§7Search: §f\"" + searchText + "\" §7(§f" + searchText.length() + "§7 chars)", x, y, textColor); + y += lineHeight; + } + } + + // 显示调试提示 + guiGraphics.drawString(this.font, "§8Press 'Dev Toggle' to hide this panel", x, y, 0x888888); + } + + /** + * 获取当前服务器名称 + */ + private String getCurrentServerName() { + try { + if (Minecraft.getInstance().getCurrentServer() != null) { + return Minecraft.getInstance().getCurrentServer().name; + } + } catch (Exception e) { + // 忽略 + } + return "SinglePlayer/Local"; + } + + /** + * 获取在线玩家数量 + */ + private int getOnlinePlayerCount() { + try { + if (Minecraft.getInstance().getConnection() != null) { + return Minecraft.getInstance().getConnection().getOnlinePlayers().size(); + } + } catch (Exception e) { + // 忽略错误 + } + return 0; } @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - // 先检查按钮点击 + // 先检查搜索框点击 - 让搜索框获得焦点 + if (searchBox != null) { + boolean clicked = searchBox.mouseClicked(mouseX, mouseY, button); + if (clicked) { + // 点击搜索框时,取消列表的焦点,并确保搜索框获得焦点 + if (serverList != null) { + serverList.setFocused(false); + } + searchBox.setFocused(true); + return true; + } + } + + // 再检查按钮点击 if (enableCrCheckBox != null && enableCrCheckBox.mouseClicked(mouseX, mouseY, button)) { return true; } @@ -145,15 +407,57 @@ public class CrossServerGui extends Screen { if (closeButton != null && closeButton.mouseClicked(mouseX, mouseY, button)) { return true; } + if (DEV_MODE) { + if (devTestButton != null && devTestButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + if (devToggleDebugButton != null && devToggleDebugButton.mouseClicked(mouseX, mouseY, button)) { + return true; + } + } - // 再检查列表点击 + // 最后检查列表点击 if (serverList != null && serverList.mouseClicked(mouseX, mouseY, button)) { return true; } + // 点击其他地方时,取消搜索框焦点 + if (searchBox != null && searchBox.isFocused()) { + searchBox.setFocused(false); + } + return super.mouseClicked(mouseX, mouseY, button); } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + // ESC键关闭GUI + if (keyCode == GLFW.GLFW_KEY_ESCAPE) { + this.onClose(); + return true; + } + + // 优先让搜索框处理按键(如果搜索框有焦点) + if (searchBox != null && searchBox.isFocused()) { + if (searchBox.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) { + // 优先让搜索框处理字符输入 + if (searchBox != null && searchBox.isFocused()) { + if (searchBox.charTyped(codePoint, modifiers)) { + return true; + } + } + return super.charTyped(codePoint, modifiers); + } + void sendCustomPayload(String message) { Minecraft mc = Minecraft.getInstance(); if (mc.getConnection() != null) { @@ -164,11 +468,11 @@ public class CrossServerGui extends Screen { } private void renderLogo(@NotNull GuiGraphics guiGraphics) { - int logoWidth = 64; // 缩小Logo,为列表腾出空间 + int logoWidth = 64; int logoHeight = 64; int x = (this.width - logoWidth - font.width(this.title.getString()) * 2) / 2; - int y = -5; // 更靠近顶部 + int y = -5; guiGraphics.blit(LOGO_TEXTURE, x, y, 0, 0, logoWidth, logoHeight, logoWidth, logoHeight); } diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/ServerSelectionList.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/ServerSelectionList.java index f3e87f0..ca5a474 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/ServerSelectionList.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/ServerSelectionList.java @@ -1,5 +1,6 @@ package com.leisuretimedock.crossmod.client.gui; +import com.leisuretimedock.crossmod.client.KeyBindingHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.Button; @@ -7,25 +8,272 @@ import net.minecraft.client.gui.components.ObjectSelectionList; import net.minecraft.network.chat.Component; import org.jetbrains.annotations.NotNull; -import java.util.Map; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class ServerSelectionList extends ObjectSelectionList { private final CrossServerGui parentScreen; + private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{(\\w+)\\}"); + private static final Pattern FORMAT_PATTERN = Pattern.compile("%([dsf])"); + + // 存储所有服务器条目用于过滤 + private final List allEntries = new ArrayList<>(); + private String searchFilter = ""; public ServerSelectionList(CrossServerGui parent, Minecraft mc, int width, int height, int y0, int y1, int itemHeight, @NotNull Map servers) { super(mc, width, height, y0, y1, itemHeight); this.parentScreen = parent; - // 添加服务器条目 + // 创建所有条目 if (servers.isEmpty()) { - this.addEntry(new ServerEntry(Component.translatable("ltd.mod.client.menu.button.no_servers"), null, parentScreen)); + ServerEntry entry = new ServerEntry(Component.translatable("ltd.mod.client.menu.button.no_servers"), null, parentScreen); + allEntries.add(entry); + this.addEntry(entry); } else { - servers.forEach((server_name, translate_key) -> { - this.addEntry(new ServerEntry(Component.translatable(translate_key), server_name, parentScreen)); + servers.forEach((serverId, translateKey) -> { + Component displayName = formatComponent(translateKey, serverId); + ServerEntry entry = new ServerEntry(displayName, serverId, parentScreen); + allEntries.add(entry); + this.addEntry(entry); }); } } + /** + * 设置搜索过滤器 + */ + public void setSearchFilter(String filterText) { + this.searchFilter = filterText == null ? "" : filterText.trim().toLowerCase(); + refreshEntries(); + } + + /** + * 刷新列表显示(应用当前过滤) + */ + private void refreshEntries() { + this.clearEntries(); + + if (searchFilter.isEmpty()) { + // 无过滤,显示所有条目 + allEntries.forEach(this::addEntry); + } else { + // 过滤条目 + boolean hasMatch = false; + for (ServerEntry entry : allEntries) { + if (entry.serverId == null) { + // "无服务器"条目只在没有其他条目时显示 + continue; + } + String displayText = entry.displayName.getString().toLowerCase(); + String serverId = entry.serverId.toLowerCase(); + if (displayText.contains(searchFilter) || serverId.contains(searchFilter)) { + this.addEntry(entry); + hasMatch = true; + } + } + // 如果没有匹配的条目,显示提示 + if (!hasMatch && !allEntries.isEmpty()) { + Component noMatch = Component.literal("§7没有匹配的服务器: " + searchFilter); + this.addEntry(new ServerEntry(noMatch, null, parentScreen)); + } + } + } + + private Component formatComponent(String input, String serverId) { + if (input == null || input.isEmpty()) { + return Component.empty(); + } + + try { + // 检查是否包含格式化参数 + if (input.contains("?")) { + String[] parts = input.split("\\?", 2); + String key = parts[0]; + String paramString = parts[1]; + + // 解析参数 + Map params = parseParameters(paramString); + + // 获取翻译文本 + String template = Component.translatable(key).getString(); + + // 先替换 {placeholder} 格式 + String formatted = replacePlaceholders(template, params, serverId); + + // 再处理 %s, %d, %f 格式 + formatted = handleStringFormat(formatted, params, serverId); + + return Component.literal(formatted); + } else { + // 没有参数,直接翻译 + return Component.translatable(input); + } + } catch (Exception e) { + // 出错时回退到原始文本 + return Component.literal(input); + } + } + + private @NotNull Map parseParameters(@NotNull String paramString) { + Map params = new HashMap<>(); + String[] pairs = paramString.split("&"); + + for (String pair : pairs) { + String[] kv = pair.split("=", 2); + if (kv.length == 2) { + params.put(kv[0], kv[1]); + } + } + + return params; + } + + private @NotNull String replacePlaceholders(String template, Map params, String serverId) { + Matcher matcher = PLACEHOLDER_PATTERN.matcher(template); + StringBuilder result = new StringBuilder(); + + while (matcher.find()) { + String placeholder = matcher.group(1); + String replacement = getReplacement(placeholder, params, serverId); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + } + matcher.appendTail(result); + + return result.toString(); + } + + private String getReplacement(String placeholder, @NotNull Map params, String serverId) { + // 首先检查参数映射 + if (params.containsKey(placeholder)) { + String value = params.get(placeholder); + // 如果值是占位符,递归处理 + if (value != null && value.startsWith("{") && value.endsWith("}")) { + String innerPlaceholder = value.substring(1, value.length() - 1); + return getReplacement(innerPlaceholder, params, serverId); + } + return value != null ? value : ""; + } + + // 内置占位符 + return switch (placeholder) { + case "serverId" -> serverId != null ? serverId : "Unknown Server"; + case "player" -> { + try { + yield Minecraft.getInstance().getUser().getName(); + } catch (Exception e) { + yield "Unknown Player"; + } + } + case "playerCount" -> String.valueOf(getOnlinePlayerCount()); + case "serverName" -> Minecraft.getInstance().getCurrentServer() != null ? + Minecraft.getInstance().getCurrentServer().name : "Unknown Server"; + case "currentTime" -> String.valueOf(System.currentTimeMillis()); + case "ip" -> Minecraft.getInstance().getCurrentServer() != null ? + Minecraft.getInstance().getCurrentServer().ip : "Unknown IP"; + case "version" -> Minecraft.getInstance().getVersionType(); + case "key" -> { + try { + // 获取打开菜单的按键名称 + yield KeyBindingHandler.OPEN_GUI_KEY.getKey().getDisplayName().getString(); + } catch (Exception e) { + yield "Unknown Key"; + } + } + default -> "{" + placeholder + "}"; + }; + } + + private String handleStringFormat(String text, Map params, String serverId) { + Matcher matcher = FORMAT_PATTERN.matcher(text); + StringBuilder result = new StringBuilder(); + int index = 0; + + while (matcher.find()) { + String formatType = matcher.group(1); + String replacement = getStringFormatReplacement(formatType, index, params, serverId); + matcher.appendReplacement(result, Matcher.quoteReplacement(replacement)); + index++; + } + matcher.appendTail(result); + + return result.toString(); + } + + private String getStringFormatReplacement(String formatType, int index, Map params, String serverId) { + // 尝试从参数中获取值 + String paramValue = null; + + // 尝试不同的参数键名 + String[] possibleKeys = { + "arg" + index, + "param" + index, + "p" + index, + String.valueOf(index) + }; + + for (String key : possibleKeys) { + if (params.containsKey(key)) { + paramValue = params.get(key); + break; + } + } + + // 如果没有找到参数,使用默认值 + if (paramValue == null) { + paramValue = getDefaultFormattedValue(index, serverId); + } + + // 根据格式类型格式化 + try { + return switch (formatType) { + case "d" -> { + try { + yield String.valueOf(Integer.parseInt(paramValue)); + } catch (NumberFormatException e) { + yield "0"; + } + } + case "f" -> { + try { + yield String.format("%.1f", Double.parseDouble(paramValue)); + } catch (NumberFormatException e) { + yield "0.0"; + } + } + default -> paramValue; + }; + } catch (Exception e) { + return paramValue; + } + } + + private String getDefaultFormattedValue(int index, String serverId) { + return switch (index) { + case 0 -> serverId != null ? serverId : "Server"; + case 1 -> String.valueOf(getOnlinePlayerCount()); + case 2 -> { + try { + yield Minecraft.getInstance().getUser().getName(); + } catch (Exception e) { + yield "Player"; + } + } + default -> "?"; + }; + } + + private int getOnlinePlayerCount() { + try { + if (Minecraft.getInstance().getConnection() != null) { + return Minecraft.getInstance().getConnection().getOnlinePlayers().size(); + } + } catch (Exception e) { + // 忽略错误 + } + return 0; + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (button == 0) { @@ -38,6 +286,7 @@ public class ServerSelectionList extends ObjectSelectionList { private final Component displayName; private final String serverId; @@ -89,4 +338,4 @@ public class ServerSelectionList extends ObjectSelectionList> SERVER_LIST; public static final ForgeConfigSpec.BooleanValue DISABLED_JOIN_QUIT_MESSAGE; + static { BUILDER.comment("Cross Server Config").push("servers"); SERVER_LIST = BUILDER - .comment("Server list in format: : ") + .comment( + "Server list in format: : ", + "", + "Supported formats:", + " 1. Translation key: lobby: ltd.mod.client.menu.button.hub", + " 2. Translation key with parameters: survival: ltd.mod.client.menu.button.survival?player={player}&count={playerCount}", + " 3. Direct text: custom1: H Custom Lobby", + " 4. Direct text with color: custom2: §a[Hub] §fServer", + "", + "Available placeholders:", + " {player} - Current player name", + " {playerCount} - Online players count", + " {serverId} - Server ID", + " {serverName} - Current server name", + " {currentTime} - Current timestamp", + " {ip} - Server IP", + " {version} - Minecraft version", + " {key} - Hotkey name", + "", + "Examples:", + " lobby: ltd.mod.client.menu.button.hub", + " survival: ltd.mod.client.menu.button.survival?player={player}", + " custom: H My Server", + " test: ltd.mod.client.menu.button.test?player={player}&count={playerCount}" + ) .defineList("serverList", Arrays.asList( "lobby: ltd.mod.client.menu.button.hub", - "survival: ltd.mod.client.menu.button.survival" + "survival: ltd.mod.client.menu.button.survival", + "skyblock: ltd.mod.client.menu.button.skyblock", + "resource: ltd.mod.client.menu.button.resource", + "minigame: ltd.mod.client.menu.button.minigame" ), - obj -> obj instanceof String str && checkSyntax(str) + obj -> obj instanceof String str && CrossServerConfigManager.SYNTAX.matcher(str).matches() ); BUILDER.pop(); - DISABLED_JOIN_QUIT_MESSAGE = BUILDER.comment("Disable join or quit message") - .define("disabled_join_quit_message", false); + + DISABLED_JOIN_QUIT_MESSAGE = BUILDER + .comment("Disable join or quit message") + .define("disabled_join_quit_message", false); + SPEC = BUILDER.build(); } - public static boolean checkSyntax(@NotNull String input) { - return CrossServerConfigManager.SYNTAX.matcher(input).matches(); - } -} +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/config/CrossServerConfigManager.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/config/CrossServerConfigManager.java index aed98cf..00fe2af 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/config/CrossServerConfigManager.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/config/CrossServerConfigManager.java @@ -17,8 +17,18 @@ import static java.util.regex.Pattern.compile; @Slf4j public class CrossServerConfigManager { public static CrossServerConfigManager INSTANCE = new CrossServerConfigManager(); - public static final Pattern SYNTAX = - compile("([a-zA-Z]\\w+):\\s+([_.\\w]+)"); + + // 支持更灵活的格式: server_name: translate_key?param1=value1¶m2=value2 + // 或者 server_name: translate_key + // 或者 server_name: 直接文本 + public static final Pattern SYNTAX = compile( + "^([a-zA-Z]\\w+):\\s*(.+)$" // 匹配 server_name: 后面的任何内容 + ); + + // 用于解析参数 + private static final Pattern PARAM_PATTERN = compile("\\?(.+)$"); + private static final Pattern KEY_PATTERN = compile("^([a-zA-Z_.]+)"); + /** * The constant cacheTag. */ @@ -35,23 +45,128 @@ public class CrossServerConfigManager { @Getter private boolean disabledJoinQuitMessage = false; + /** + * 解析服务器配置,支持带参数的翻译键 + * + * @param servers 配置列表 + * @return 不可修改的服务器映射 + */ private @NotNull @Unmodifiable Map parseServer(@NotNull List servers) { Map serverMap = new TreeMap<>(); for (String server : servers) { Matcher matcher = SYNTAX.matcher(server); if (matcher.matches()) { - String key = matcher.group(1); // 第一部分:[a-zA-Z]\w+ - String value = matcher.group(2); // 第二部分:[_.\w]+ - if(!serverMap.containsKey(key)) { - serverMap.put(key, value); + String key = matcher.group(1).trim(); // 服务器ID + String value = matcher.group(2).trim(); // 翻译键或直接文本 + + if (!serverMap.containsKey(key)) { + // 验证并规范化值 + String normalizedValue = normalizeValue(value); + serverMap.put(key, normalizedValue); + log.debug("Loaded server: {} -> {}", key, normalizedValue); } else { log.warn("Duplicate server name '{}' found in config, skip it", key); } + } else { + log.warn("Invalid server entry format: '{}', expected: 'server_name: translate_key'", server); } } return Collections.unmodifiableMap(serverMap); } + /** + * 规范化值,确保格式正确 + * 支持: + * 1. 纯翻译键: "ltd.mod.client.menu.button.hub" + * 2. 带参数的翻译键: "ltd.mod.client.menu.button.hub?player={player}&count={playerCount}" + * 3. 直接文本: "🏠 Hub Server" + * 4. 带格式的文本: "§a[Hub] §fServer" + */ + private String normalizeValue(String value) { + if (value == null || value.trim().isEmpty()) { + return value; + } + + value = value.trim(); + + // 检查是否是翻译键格式 (包含字母、数字、下划线、点) + Matcher keyMatcher = KEY_PATTERN.matcher(value); + if (keyMatcher.find()) { + String potentialKey = keyMatcher.group(1); + // 如果匹配到的是翻译键格式,保留原样 + if (potentialKey.matches("^[a-zA-Z_][a-zA-Z0-9_.]*$")) { + return value; + } + } + + // 如果不是翻译键格式,作为直接文本处理 + // 检查是否包含参数 + if (value.contains("?")) { + // 可能是格式: "text?param=value" + String[] parts = value.split("\\?", 2); + String prefix = parts[0]; + // 如果前缀看起来像翻译键,保留;否则作为文本 + if (prefix.matches("^[a-zA-Z_][a-zA-Z0-9_.]*$")) { + return value; // 保留完整的翻译键+参数格式 + } + } + + // 默认返回原值 + return value; + } + + /** + * 获取带参数的翻译键 + * 如果值是翻译键格式,返回原值;否则返回null + */ + public String getTranslateKey(String serverId) { + String value = servers.get(serverId); + if (value == null) return null; + + // 检查是否是翻译键格式(包含字母、数字、下划线、点) + if (value.matches("^[a-zA-Z_][a-zA-Z0-9_.]*\\??.*$")) { + return value; + } + return null; + } + + /** + * 获取直接文本 + * 如果值不是翻译键格式,返回原值;否则返回null + */ + public String getDirectText(String serverId) { + String value = servers.get(serverId); + if (value == null) return null; + + // 如果不是翻译键格式,作为直接文本 + if (!value.matches("^[a-zA-Z_][a-zA-Z0-9_.]*\\??.*$")) { + return value; + } + return null; + } + + /** + * 检查服务器是否使用翻译键 + */ + public boolean isTranslateKey(String serverId) { + return getTranslateKey(serverId) != null; + } + + /** + * 获取服务器显示文本(用于搜索) + */ + public String getDisplayText(String serverId) { + String value = servers.get(serverId); + if (value == null) return serverId; + + // 如果是翻译键,返回翻译键本身(将在ServerSelectionList中解析) + if (value.matches("^[a-zA-Z_][a-zA-Z0-9_.]*\\??.*$")) { + return value; + } + // 否则返回直接文本 + return value; + } + public void reloadAll() { try { clear(); @@ -59,13 +174,20 @@ public class CrossServerConfigManager { disabledJoinQuitMessage = CrossServerConfig.DISABLED_JOIN_QUIT_MESSAGE.get(); cacheHash = -1; cacheTag = serializeToNBT(); - log.debug("Configs reloaded"); + log.info("Configs reloaded, loaded {} servers", servers.size()); + + // 打印加载的服务器列表(用于调试) + if (!servers.isEmpty()) { + log.debug("Loaded servers:"); + servers.forEach((id, value) -> + log.debug(" {} -> {}", id, value) + ); + } } catch (Exception e) { log.error("Failed to reload configs", e); cacheHash = -1; cacheTag = null; } - } /** @@ -84,6 +206,7 @@ public class CrossServerConfigManager { } CompoundTag tag = new CompoundTag(); serializeMap(tag, "servers", this.servers); + tag.putBoolean("disabledJoinQuitMessage", disabledJoinQuitMessage); cacheHash = calculateConfigHash(); cacheTag = tag; return tag; @@ -98,7 +221,6 @@ public class CrossServerConfigManager { parent.put(key, mapTag); } - /** * 从NBT反序列化配置管理器状态 * @@ -109,6 +231,9 @@ public class CrossServerConfigManager { cacheTag = null; clear(); deserializeMap(tag, "servers", servers); + if (tag.contains("disabledJoinQuitMessage")) { + disabledJoinQuitMessage = tag.getBoolean("disabledJoinQuitMessage"); + } cacheTag = serializeToNBT(); } @@ -156,6 +281,8 @@ public class CrossServerConfigManager { int hash = 0x811c9dc5; // FNV偏移基础值 TreeMap sortedMap = new TreeMap<>(servers); hash = fnv1aHashMap(hash, sortedMap); + // 包含 disabledJoinQuitMessage 在哈希中 + hash = fnv1aHashString(hash, String.valueOf(disabledJoinQuitMessage)); return hash; } @@ -166,11 +293,11 @@ public class CrossServerConfigManager { } return hash; } + private int fnv1aHashMap(int hash, @NotNull Map map) { for (Map.Entry entry : map.entrySet()) { hash = fnv1aHashString(hash, entry.getKey()); hash = fnv1aHashString(hash, entry.getValue()); - } return hash; } @@ -183,4 +310,25 @@ public class CrossServerConfigManager { NetworkHandler.sendToAllPlayer(new CommonConfigHashInformPacket(cacheHash)); } } -} + + /** + * 获取服务器数量 + */ + public int getServerCount() { + return servers.size(); + } + + /** + * 检查服务器是否存在于配置中 + */ + public boolean hasServer(String serverId) { + return servers.containsKey(serverId); + } + + /** + * 获取服务器配置值 + */ + public String getServerValue(String serverId) { + return servers.get(serverId); + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/MixinPlayerList.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/MixinPlayerList.java index 2e4c2d1..8345f5a 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/MixinPlayerList.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/MixinPlayerList.java @@ -9,6 +9,7 @@ import org.slf4j.Logger; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -30,7 +31,7 @@ public class MixinPlayerList { cancellable = true ) private void onBroadcastSystemMessage(Component message, boolean bypassHiddenChat, CallbackInfo ci) { - if (shouldCancel(message)) { + if (crossServerTeleport$shouldCancel(message)) { ci.cancel(); } } @@ -44,12 +45,13 @@ public class MixinPlayerList { cancellable = true ) private void onBroadcastSystemMessage(Component serverMessage, Function playerMessageFactory, boolean bypassHiddenChat, CallbackInfo ci) { - if (shouldCancel(serverMessage)) { + if (crossServerTeleport$shouldCancel(serverMessage)) { ci.cancel(); } } - private boolean shouldCancel(Component message) { + @Unique + private boolean crossServerTeleport$shouldCancel(Component message) { if (!CrossServerConfigManager.INSTANCE.isDisabledJoinQuitMessage()) { return false; } diff --git a/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/en_us.json b/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/en_us.json index d59e8cd..a8b7dc9 100644 --- a/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/en_us.json +++ b/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/en_us.json @@ -7,6 +7,8 @@ "ltd.mod.client.menu.button.skyblock": "S SkyBlock", "ltd.mod.client.menu.button.resource": "R Resource", "ltd.mod.client.menu.button.minigame": "M MiniGame", + "ltd.mod.client.menu.button.custom": "{icon} ${name}", + "ltd.mod.client.menu.search": "Search servers...", "ltd.mod.client.menu.checkbox.show_trans_tip": "Show cross server tip overlay", "ltd.mod.client.menu.checkbox.show_ping_stat": "Show ping stat overlay", "ltd.mod.client.negotiating": "Negotiating...", diff --git a/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/zh_cn.json b/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/zh_cn.json index e531892..b8ee55d 100644 --- a/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/zh_cn.json +++ b/forge-mod/src/main/resources/assets/ltdcrossteleport/lang/zh_cn.json @@ -7,6 +7,7 @@ "ltd.mod.client.menu.button.skyblock": "S 空岛服", "ltd.mod.client.menu.button.resource": "R 资源服", "ltd.mod.client.menu.button.minigame": "M 小游戏服", + "ltd.mod.client.menu.search": "搜索服务器..", "ltd.mod.client.menu.checkbox.show_trans_tip": "显示传送提示", "ltd.mod.client.menu.checkbox.show_ping_stat": "显示Ping状态", "ltd.mod.client.negotiating": "重定向中 ...", @@ -56,5 +57,9 @@ "crossmod.command.debug.enabled": "已开启", "crossmod.command.debug.disabled": "已关闭", "crossmod.command.debug.enabled_short": "§a已开启", - "crossmod.command.debug.disabled_short": "§c已关闭" + "crossmod.command.debug.disabled_short": "§c已关闭", + "ltd.mod.client.menu.button.1": "H 大厅", + "ltd.mod.client.menu.button.2": "S 生存", + "ltd.mod.request.goto": "请求跳转 %s", + "ltd.mod.client.menu.button.custom": "{icon} ${name}" } \ No newline at end of file