添加更多参数化支持

This commit is contained in:
叁玖领域 2026-06-26 14:27:54 +08:00
parent 20330df2a9
commit 119066dd6d
9 changed files with 800 additions and 46 deletions

View File

@ -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

View File

@ -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");
}
}
}

View File

@ -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<String, String> 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<String, String> 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);
}

View File

@ -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<ServerSelectionList.ServerEntry> {
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<ServerEntry> allEntries = new ArrayList<>();
private String searchFilter = "";
public ServerSelectionList(CrossServerGui parent, Minecraft mc, int width, int height, int y0, int y1, int itemHeight, @NotNull Map<String, String> 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<String, String> 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<String, String> parseParameters(@NotNull String paramString) {
Map<String, String> 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<String, String> 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<String, String> 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<String, String> 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<String, String> 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<ServerSelectionList
}
return super.mouseClicked(mouseX, mouseY, button);
}
public static class ServerEntry extends ObjectSelectionList.Entry<ServerEntry> {
private final Component displayName;
private final String serverId;
@ -89,4 +338,4 @@ public class ServerSelectionList extends ObjectSelectionList<ServerSelectionList
return displayName;
}
}
}
}

View File

@ -1,7 +1,6 @@
package com.leisuretimedock.crossmod.config;
import net.minecraftforge.common.ForgeConfigSpec;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.List;
@ -11,23 +10,51 @@ public class CrossServerConfig {
public static ForgeConfigSpec SPEC;
public static final ForgeConfigSpec.ConfigValue<List<? extends String>> 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: <server_name>: <translate_key>")
.comment(
"Server list in format: <server_name>: <translate_key>",
"",
"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();
}
}
}

View File

@ -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&param2=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<String, String> parseServer(@NotNull List<? extends String> servers) {
Map<String, String> 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<String, String> 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<String, String> map) {
for (Map.Entry<String, String> 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);
}
}

View File

@ -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<ServerPlayer, Component> 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;
}

View File

@ -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...",

View File

@ -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}"
}