From 6f045b681f2249fabf4a1cc3478fb4df327eb1e8 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Fri, 25 Jul 2025 02:03:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E4=BA=86ping=E5=BB=B6?= =?UTF-8?q?=E8=BF=9F=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/misc.xml | 1 - forge-mod/build.gradle | 5 +- .../crossmod/CrossTeleportMod.java | 62 ++- .../crossmod/NetworkHandler.java | 54 --- .../crossmod/client/ClientPingHandler.java | 140 ++++++ .../crossmod/client/KeyBindingHandler.java | 1 + .../crossmod/client/OverlayRenderer.java | 55 --- .../crossmod/client/PluginChannelClient.java | 24 +- .../crossmod/client/command/GotoCommand.java | 24 + .../client/{ => gui}/CrossServerGui.java | 33 +- .../client/overlay/CrossServerTipOverLay.java | 45 ++ .../client/overlay/OverlayRenderer.java | 27 ++ .../client/overlay/PingOverlayManager.java | 116 +++++ .../client/{ => overlay}/PluginCommand.java | 2 +- .../crossmod/command/PingCommand.java | 275 +++++++++++ .../mixin/CrossServerModMixinPlugin.java | 45 ++ .../crossmod/network/NetworkHandler.java | 155 +++++++ .../crossmod/network/PingRequestManager.java | 436 ++++++++++++++++++ .../{ => network}/PluginMessageListener.java | 2 +- .../network/toClient/PingMessagePayload.java | 34 ++ .../network/toClient/PingResultPacket.java | 57 +++ .../network/toClient/PingStatsPacket.java | 46 ++ .../network/toClient/ResetPacket.java | 66 +++ .../network/toServer/PongMessagePayload.java | 29 ++ .../crossmod/reset/ClientResetManager.java | 9 +- .../crossmod/reset/ResetHelper.java | 3 - .../assets/ltdcrossteleport/lang/en_us.json | 34 +- .../assets/ltdcrossteleport/lang/zh_cn.json | 34 +- .../resources/ltdcrossteleport.mixins.json | 7 +- velocity-plugin/build.gradle | 2 + .../crossplugin/CrossPlugin.java | 39 +- .../crossplugin/command/PingCommand.java | 4 + .../listener/PingMessageListener.java | 68 +++ .../listener/PluginMessageListener.java | 36 +- .../crossplugin/manager/ConfigManager.java | 90 +++- .../crossplugin/manager/PingManager.java | 60 +++ .../crossplugin/manager/ServerManager.java | 6 +- velocity-plugin/src/main/resources/config.yml | 35 ++ 38 files changed, 1979 insertions(+), 182 deletions(-) delete mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/NetworkHandler.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/ClientPingHandler.java delete mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/OverlayRenderer.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/command/GotoCommand.java rename forge-mod/src/main/java/com/leisuretimedock/crossmod/client/{ => gui}/CrossServerGui.java (66%) create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/CrossServerTipOverLay.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/OverlayRenderer.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PingOverlayManager.java rename forge-mod/src/main/java/com/leisuretimedock/crossmod/client/{ => overlay}/PluginCommand.java (87%) create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/command/PingCommand.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/CrossServerModMixinPlugin.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/NetworkHandler.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PingRequestManager.java rename forge-mod/src/main/java/com/leisuretimedock/crossmod/{ => network}/PluginMessageListener.java (97%) create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingMessagePayload.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingResultPacket.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingStatsPacket.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/ResetPacket.java create mode 100644 forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toServer/PongMessagePayload.java create mode 100644 velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/command/PingCommand.java create mode 100644 velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PingMessageListener.java create mode 100644 velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/PingManager.java diff --git a/.idea/misc.xml b/.idea/misc.xml index d285eb1..cae3721 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/forge-mod/build.gradle b/forge-mod/build.gradle index 679f4a8..d288b51 100644 --- a/forge-mod/build.gradle +++ b/forge-mod/build.gradle @@ -27,6 +27,8 @@ configurations { attribute(Attribute.of("net.neoforged.moddevgradle.legacy.minecraft_mappings.v2", String), "named") } } + clientModImplementation + implementation.extendsFrom clientModImplementation } println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}" @@ -94,10 +96,11 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.24' annotationProcessor 'org.projectlombok:lombok:1.18.24' annotationProcessor 'org.spongepowered:mixin:0.8.5:processor' + modImplementation "curse.maven:easy-villagers-400514:3887794" modImplementation "curse.maven:xaeros-world-map-317780:6538320" modImplementation "curse.maven:immersive-aircraft-666014:4679496" - modImplementation "curse.maven:modern-ui-352491:5229350" + modCompileOnly "curse.maven:modern-ui-352491:5229350" modImplementation "curse.maven:iceberg-520110:4035917" } 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 898dbd8..30cbe8c 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/CrossTeleportMod.java @@ -1,17 +1,30 @@ package com.leisuretimedock.crossmod; +import com.leisuretimedock.crossmod.command.PingCommand; +import com.leisuretimedock.crossmod.network.NetworkHandler; +import com.leisuretimedock.crossmod.network.PingRequestManager; import com.leisuretimedock.crossmod.reset.ClientResetManager; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.event.RegisterCommandsEvent; +import net.minecraftforge.event.entity.player.PlayerEvent; +import net.minecraftforge.event.server.ServerStartedEvent; +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.ModLoadingContext; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraftforge.fml.event.lifecycle.FMLDedicatedServerSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.network.NetworkConstants; +import org.jetbrains.annotations.Nullable; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.Objects; +import java.util.UUID; @Mod(CrossTeleportMod.MOD_ID) public class CrossTeleportMod { @@ -23,14 +36,55 @@ public class CrossTeleportMod { ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> NetworkConstants.IGNORESERVERONLY, (a, b) -> true)); IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); - modEventBus.addListener(ClientResetManager::init); - + if(!FMLEnvironment.dist.isDedicatedServer()) modEventBus.addListener(ClientResetManager::init); + NetworkHandler.register(); + } + @Mod.EventBusSubscriber(modid = MOD_ID, bus = Mod.EventBusSubscriber.Bus.FORGE) + public static class CommonEvents { + @Nullable + public static MinecraftServer server; + @SubscribeEvent + public static void onRegisterCommands(RegisterCommandsEvent event) { + PingCommand.register(event.getDispatcher()); + } + @SubscribeEvent + public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + PingRequestManager.monitor(player); + } + } + @SubscribeEvent + public static void onPlayerLoggedOut(PlayerEvent.PlayerLoggedOutEvent event) { + if (event.getEntity() instanceof ServerPlayer player) { + PingRequestManager.unmonitor(player); + } + } + @SubscribeEvent + public static void onServerStart(ServerStartedEvent event) { + server = event.getServer(); + } + @SubscribeEvent + public static void onServerStop(ServerStoppedEvent event) { + server = null; + } + public static ServerPlayer getPlayerByUUID(UUID uuid) { + Objects.requireNonNull(server, "server is null"); + return server.getPlayerList().getPlayer(uuid); + } } @Mod.EventBusSubscriber(modid = MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE) public static class ClientEvents { @SubscribeEvent public static void onClientSetup(FMLClientSetupEvent event) { - event.enqueueWork(NetworkHandler::register); + } } + @Mod.EventBusSubscriber(modid = MOD_ID, value = Dist.DEDICATED_SERVER, bus = Mod.EventBusSubscriber.Bus.FORGE) + public static class ServerEvent { + @SubscribeEvent + public static void onServerSetup(FMLDedicatedServerSetupEvent event) { + + } + + } } diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/NetworkHandler.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/NetworkHandler.java deleted file mode 100644 index 6a47a8b..0000000 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/NetworkHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -// 客户端网络处理类(CrossMod 端) -package com.leisuretimedock.crossmod; - -import io.netty.buffer.Unpooled; -import net.minecraft.client.Minecraft; -import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; -import net.minecraft.resources.ResourceLocation; - -import java.util.Objects; - -/** - * NetworkHandler 用于客户端向服务端发送插件消息。 - * 目前只用 plugin message 方式进行通信。 - */ -public class NetworkHandler { - - // 自定义插件消息通道标识 - public static final ResourceLocation CHANNEL_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "channel"); - - public static void register() { - // TODO: 未来支持双端注册,以便服务器端也能处理相关命令 - // 当前仅客户端发送 PluginMessage,无需额外注册 - } - - /** - * 发送自定义插件消息 - * @param subChannel 子通道标识 - * @param payload 负载数据(字节数组) - */ - public static void sendPluginMessage(ResourceLocation subChannel, byte[] payload) { - FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(payload.length)); - buf.writeBytes(payload); - - // 获取当前连接并发送自定义负载包 - Objects.requireNonNull(Minecraft.getInstance().getConnection()) - .send(new ServerboundCustomPayloadPacket(subChannel, buf)); - } - - /** - * 发送客户端已准备好消息(示例方法,调用具体实现) - */ - public static void sendClientReady() { - PluginMessageListener.sendClientReady(); - } - - /** - * 发送传送请求到代理服务器 - * @param serverName 目标服务器名 - */ - public static void sendTeleportRequest(String serverName) { - PluginMessageListener.sendTeleport(serverName); - } -} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/ClientPingHandler.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/ClientPingHandler.java new file mode 100644 index 0000000..9a04c26 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/ClientPingHandler.java @@ -0,0 +1,140 @@ +package com.leisuretimedock.crossmod.client; + +import com.leisuretimedock.crossmod.network.PingRequestManager; +import net.minecraft.client.Minecraft; +import net.minecraft.world.entity.player.Player; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +public class ClientPingHandler { + private static final long DATA_EXPIRE_TIME = 10000; // 10秒数据有效期 + private static final Map pingData = new ConcurrentHashMap<>(); + private static PingRequestManager.PingStats lastStats; + private static long lastStatsUpdateTime; + + public static void handlePingResults(Map pingResults, Map averages) { + long now = System.currentTimeMillis(); + + pingResults.forEach((uuid, ping) -> { + PingData data = pingData.computeIfAbsent(uuid, k -> new PingData()); + data.update(ping, averages.getOrDefault(uuid, 0.0), now); + }); + + // 清理过期的数据(5秒未更新) + long currentTime = System.currentTimeMillis(); + pingData.entrySet().removeIf(entry -> + currentTime - entry.getValue().lastUpdateTime > 5000 + ); + } + + public static String getPingDisplayText() { + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) return ""; + + StringBuilder sb = new StringBuilder(); + + // 显示自己的ping信息 + Player localPlayer = mc.player; + if (localPlayer != null) { + PingData selfData = pingData.get(localPlayer.getUUID()); + if (selfData != null) { + sb.append("Ping: ").append(selfData.currentPing).append("ms"); + sb.append(" (Avg: ").append(String.format("%.1f", selfData.averageLatency)).append("ms)"); + } + } + + // 显示其他玩家的ping信息(最多3个) + int count = 0; + for (Map.Entry entry : pingData.entrySet()) { + if (count >= 3) break; + if (localPlayer != null && entry.getKey().equals(localPlayer.getUUID())) continue; + + Player player = mc.level.getPlayerByUUID(entry.getKey()); + if (player != null) { + sb.append("\n"); + sb.append(player.getScoreboardName()) + .append(": ").append(entry.getValue().currentPing).append("ms"); + count++; + } + } + + return sb.toString(); + } + + private static class PingData { + long currentPing; + double averageLatency; + long lastUpdateTime; + void update(long ping, double avgLatency, long timestamp) { + this.currentPing = ping; + this.averageLatency = avgLatency; + this.lastUpdateTime = timestamp; + } + } + + + // 更新统计数据处理方法 + public static void handlePingStats(PingRequestManager.PingStats stats) { + lastStats = stats; + lastStatsUpdateTime = System.currentTimeMillis(); + } + + // 获取要显示的统计文本 + public static String getStatsDisplayText() { + if (lastStats == null || System.currentTimeMillis() - lastStatsUpdateTime > 10000) { + return "网络统计: 数据过期"; + } + + return String.format(""" + 网络延迟统计: + 平均: %.1fms | 最高: %dms + 最低: %dms | 时延: %.1fms + 丢包率: %.1f%% | 样本: %d + """, + lastStats.average(), + lastStats.max(), + lastStats.min(), + lastStats.averageLatency(), + lastStats.packetLossRate(), + lastStats.sampleCount() + ); + } + + // 合并Ping数据和统计数据的显示方法 + public static List getCombinedDebugText() { + List lines = new ArrayList<>(); + + // 添加Ping数据 + String pingText = getPingDisplayText(); + if (!pingText.isEmpty()) { + lines.addAll(Arrays.asList(pingText.split("\n"))); + } + + // 添加统计信息 + String statsText = getStatsDisplayText(); + if (!statsText.isEmpty()) { + lines.add(""); // 空行分隔 + lines.addAll(Arrays.asList(statsText.split("\n"))); + } + + return lines; + } + + private static String resolvePlayerName(UUID uuid) { + // 实现从UUID到玩家名的解析 + if (Minecraft.getInstance().level != null) { + Player player = Minecraft.getInstance().level.getPlayerByUUID(uuid); + if (player != null) { + return player.getScoreboardName(); + } + } + return uuid.toString().substring(0, 8); + } + private static void cleanUpOldData(long currentTime) { + pingData.entrySet().removeIf(entry -> + currentTime - entry.getValue().lastUpdateTime > DATA_EXPIRE_TIME + ); + } + +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/KeyBindingHandler.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/KeyBindingHandler.java index 3de9e74..ee02e84 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/KeyBindingHandler.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/KeyBindingHandler.java @@ -1,6 +1,7 @@ package com.leisuretimedock.crossmod.client; import com.leisuretimedock.crossmod.CrossTeleportMod; +import com.leisuretimedock.crossmod.client.gui.CrossServerGui; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraftforge.api.distmarker.Dist; diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/OverlayRenderer.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/OverlayRenderer.java deleted file mode 100644 index 6d50164..0000000 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/OverlayRenderer.java +++ /dev/null @@ -1,55 +0,0 @@ -package com.leisuretimedock.crossmod.client; - -import com.leisuretimedock.crossmod.CrossTeleportMod; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.Font; -import net.minecraft.client.gui.GuiComponent; -import net.minecraft.client.renderer.entity.ItemRenderer; -import net.minecraft.world.item.ItemStack; -import net.minecraft.world.item.Items; -import net.minecraftforge.api.distmarker.Dist; -import net.minecraftforge.client.gui.OverlayRegistry; -import net.minecraftforge.eventbus.api.SubscribeEvent; -import net.minecraftforge.fml.common.Mod; -import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; - -@Mod.EventBusSubscriber(modid = CrossTeleportMod.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) -public class OverlayRenderer { - private static boolean showOverlay = false; - private static final Minecraft mc = Minecraft.getInstance(); - public static boolean isShowOverlay() { - return !showOverlay || mc.player == null || mc.level == null; - } - public static void setShow(boolean show) { - showOverlay = show; - } - - @SubscribeEvent - public static void onRender(FMLClientSetupEvent event) { - event.enqueueWork(() -> { - OverlayRegistry.registerOverlayTop( - "tran_server_tip", - (forgeIngameGui, poseStack, v, i, i1) -> { - if ( !showOverlay || mc.player == null || mc.level == null) return; - int x = 10; - int y = 10; - Font font = mc.font; - ItemRenderer itemRenderer = mc.getItemRenderer(); - - // 1. 原版钟物品 - ItemStack clockStack = new ItemStack(Items.CLOCK); - - // 2. 渲染钟图标(含动画帧) - itemRenderer.renderAndDecorateItem(clockStack, x, y); - itemRenderer.renderGuiItemDecorations(mc.font, clockStack, x, y); - - // 3. 绘制提示文字 - String keyText = KeyBindingHandler.OPEN_GUI_KEY.getTranslatedKeyMessage().getString(); // 可动态从 KeyMapping 获取 - String text = "按 [" + keyText.toUpperCase() + "] 打开跨服传送菜单"; - GuiComponent.drawString(poseStack,font, text, x + 20, y + 6, 0xFFFFFF); - - } - ); - }); - } -} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginChannelClient.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginChannelClient.java index 6440485..20afbe9 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginChannelClient.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginChannelClient.java @@ -1,18 +1,18 @@ package com.leisuretimedock.crossmod.client; import com.leisuretimedock.crossmod.CrossTeleportMod; -import com.leisuretimedock.crossmod.NetworkHandler; +import com.leisuretimedock.crossmod.client.command.GotoCommand; +import com.leisuretimedock.crossmod.client.overlay.CrossServerTipOverLay; +import com.leisuretimedock.crossmod.client.overlay.PluginCommand; +import com.leisuretimedock.crossmod.network.NetworkHandler; import com.leisuretimedock.crossmod.reset.ClientResetManager; -import com.mojang.brigadier.arguments.StringArgumentType; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import lombok.extern.slf4j.Slf4j; import net.minecraft.client.Minecraft; -import net.minecraft.commands.Commands; import net.minecraft.network.Connection; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.chat.TextComponent; import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.ClientPlayerNetworkEvent; @@ -60,11 +60,11 @@ public class PluginChannelClient { switch (cmd) { case OVERLAY_SHOW -> { log.debug("[CrossTeleportMod] 执行 OVERLAY_SHOW"); - OverlayRenderer.setShow(true); + CrossServerTipOverLay.setShow(true); } case OVERLAY_HIDE -> { log.debug("[CrossTeleportMod] 执行 OVERLAY_HIDE"); - OverlayRenderer.setShow(false); + CrossServerTipOverLay.setShow(false); } } }, () -> log.error("未知指令: {}", command)); @@ -111,16 +111,6 @@ public class PluginChannelClient { @SubscribeEvent public static void onRegisterCommand(RegisterClientCommandsEvent event) { - event.getDispatcher().register( - Commands.literal("goto") - .then(Commands.argument("server", StringArgumentType.string()) - .executes(ctx -> { - String server = StringArgumentType.getString(ctx, "server"); - NetworkHandler.sendTeleportRequest(server); - ctx.getSource().sendSuccess( - new TextComponent("请求传送到 " + server), false); - return 1; - })) - ); + GotoCommand.register(event.getDispatcher()); } } diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/command/GotoCommand.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/command/GotoCommand.java new file mode 100644 index 0000000..7cd675f --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/command/GotoCommand.java @@ -0,0 +1,24 @@ +package com.leisuretimedock.crossmod.client.command; + +import com.leisuretimedock.crossmod.network.NetworkHandler; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.TranslatableComponent; + +public class GotoCommand { + public static void register(CommandDispatcher dispatcher) { + LiteralArgumentBuilder main = Commands.literal("goto") + .then(Commands.argument("server", StringArgumentType.string()) + .executes(ctx -> { + String server = StringArgumentType.getString(ctx, "server"); + NetworkHandler.sendTeleportRequest(server); + ctx.getSource().sendSuccess( + new TranslatableComponent("ltd.mod.client.request.goto",server), false); + return 1; + })); + dispatcher.register(main); + } +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/CrossServerGui.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java similarity index 66% rename from forge-mod/src/main/java/com/leisuretimedock/crossmod/client/CrossServerGui.java rename to forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java index 5b4662d..5f74037 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/CrossServerGui.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/gui/CrossServerGui.java @@ -1,6 +1,8 @@ -package com.leisuretimedock.crossmod.client; +package com.leisuretimedock.crossmod.client.gui; import com.leisuretimedock.crossmod.CrossTeleportMod; +import com.leisuretimedock.crossmod.client.overlay.CrossServerTipOverLay; +import com.leisuretimedock.crossmod.client.overlay.PingOverlayManager; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.PoseStack; import io.netty.buffer.Unpooled; @@ -10,18 +12,21 @@ import net.minecraft.client.gui.components.Checkbox; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.renderer.GameRenderer; import net.minecraft.network.FriendlyByteBuf; -import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; 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 org.jetbrains.annotations.NotNull; +@OnlyIn(Dist.CLIENT) 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"); public CrossServerGui() { - super(new TextComponent("跨服菜单")); + super(new TranslatableComponent("ltd.mod.client.menu")); } @Override @@ -33,26 +38,36 @@ public class CrossServerGui extends Screen { int spacing = 5; addRenderableWidget(new Button(centerX - buttonWidth / 2, centerY - buttonHeight - spacing, - buttonWidth, buttonHeight, new TextComponent("🏰 主城"), btn -> { + buttonWidth, buttonHeight, new TranslatableComponent("ltd.mod.client.menu.button.1"), btn -> { sendCustomPayload("connect:lobby"); onClose(); })); addRenderableWidget(new Button(centerX - buttonWidth / 2, centerY, - buttonWidth, buttonHeight, new TextComponent("🌲 生存服"), btn -> { + buttonWidth, buttonHeight, new TranslatableComponent("ltd.mod.client.menu.button.2"), btn -> { sendCustomPayload("connect:survival"); onClose(); })); // 添加 Checkbox 控件 - Checkbox overlayCheckbox = new Checkbox(centerX - buttonWidth / 2, centerY + buttonHeight + spacing + 5, - 150, 20, new TextComponent("显示传送提示"), !OverlayRenderer.isShowOverlay()) { + Checkbox enableCrCheckBox = new Checkbox(centerX - buttonWidth / 2, centerY + buttonHeight + spacing + 5, + 150, 20, new TranslatableComponent("ltd.mod.client.menu.checkbox.show_trans_tip"), !CrossServerTipOverLay.isShowOverlay()) { @Override public void onPress() { super.onPress(); - OverlayRenderer.setShow(this.selected()); + CrossServerTipOverLay.setShow(this.selected()); } }; - addRenderableWidget(overlayCheckbox); + addRenderableWidget(enableCrCheckBox); + // 添加 Checkbox 控件 + Checkbox enablePiCheckBox = new Checkbox(centerX - buttonWidth / 2, centerY + buttonHeight + spacing + 25, + 150, 20, new TranslatableComponent("ltd.mod.client.menu.checkbox.show_ping_stat"), !PingOverlayManager.isShowOverlay()) { + @Override + public void onPress() { + super.onPress(); + PingOverlayManager.setShow(this.selected()); + } + }; + addRenderableWidget(enablePiCheckBox); } private void sendCustomPayload(String message) { diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/CrossServerTipOverLay.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/CrossServerTipOverLay.java new file mode 100644 index 0000000..741341d --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/CrossServerTipOverLay.java @@ -0,0 +1,45 @@ +package com.leisuretimedock.crossmod.client.overlay; + +import com.leisuretimedock.crossmod.client.KeyBindingHandler; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraft.client.renderer.entity.ItemRenderer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraftforge.client.gui.ForgeIngameGui; +import net.minecraftforge.client.gui.IIngameOverlay; + +public class CrossServerTipOverLay implements IIngameOverlay { + public static final CrossServerTipOverLay INSTANCE = new CrossServerTipOverLay(); + private static boolean showOverlay = false; + private static final Minecraft mc = Minecraft.getInstance(); + public static boolean isShowOverlay() { + return !showOverlay || mc.player == null || mc.level == null; + } + public static void setShow(boolean show) { + showOverlay = show; + } + @Override + public void render(ForgeIngameGui forgeIngameGui, PoseStack poseStack, float v, int i, int i1) { + if ( !showOverlay || mc.player == null || mc.level == null) return; + int x = 10; + int y = 10; + Font font = mc.font; + ItemRenderer itemRenderer = mc.getItemRenderer(); + + // 1. 原版钟物品 + ItemStack clockStack = new ItemStack(Items.CLOCK); + + // 2. 渲染钟图标(含动画帧) + itemRenderer.renderAndDecorateItem(clockStack, x, y); + itemRenderer.renderGuiItemDecorations(mc.font, clockStack, x, y); + + // 3. 绘制提示文字 + String keyText = KeyBindingHandler.OPEN_GUI_KEY.getTranslatedKeyMessage().getString(); // 可动态从 KeyMapping 获取 + String text = "按 [" + keyText.toUpperCase() + "] 打开跨服传送菜单"; + GuiComponent.drawString(poseStack, font, text, x + 20, y + 6, 0xFFFFFF); + + } +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/OverlayRenderer.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/OverlayRenderer.java new file mode 100644 index 0000000..adaf733 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/OverlayRenderer.java @@ -0,0 +1,27 @@ +package com.leisuretimedock.crossmod.client.overlay; + +import com.leisuretimedock.crossmod.CrossTeleportMod; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.gui.OverlayRegistry; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; + +@Mod.EventBusSubscriber(modid = CrossTeleportMod.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +public class OverlayRenderer { + + @SubscribeEvent + public static void onRender(FMLClientSetupEvent event) { + event.enqueueWork(() -> { + OverlayRegistry.registerOverlayTop( + "cross_server_tip", + CrossServerTipOverLay.INSTANCE + ); + OverlayRegistry.registerOverlayTop( + "ping_debug", + PingOverlayManager.INSTANCE + ); + }); + } + +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PingOverlayManager.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PingOverlayManager.java new file mode 100644 index 0000000..5de2c06 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PingOverlayManager.java @@ -0,0 +1,116 @@ +package com.leisuretimedock.crossmod.client.overlay; + +import com.leisuretimedock.crossmod.client.ClientPingHandler; +import com.mojang.blaze3d.vertex.PoseStack; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.GuiComponent; +import net.minecraftforge.client.gui.ForgeIngameGui; +import net.minecraftforge.client.gui.IIngameOverlay; + +import java.util.ArrayList; +import java.util.List; + +public class PingOverlayManager implements IIngameOverlay { + public static final PingOverlayManager INSTANCE = new PingOverlayManager(); + private static boolean showOverlay = true; + private static final Minecraft mc = Minecraft.getInstance(); + public static boolean isShowOverlay() { + return !showOverlay || mc.player == null || mc.level == null; + } + // 配置参数 + private static final int MARGIN = 5; + private static final int PADDING = 2; + private static final int BACKGROUND_COLOR = 0x80000000; + private static final int TEXT_COLOR = 0xFFFFFF; + + public static void setShow(boolean show) { + showOverlay = show; + } + + @Override + public void render(ForgeIngameGui gui, PoseStack poseStack, float partialTick, int width, int height) { + if (!showOverlay || mc.player == null || mc.level == null) { + return; + } + + // 获取所有要显示的内容 + List allLines = getAllDisplayLines(); + if (allLines.isEmpty()) { + return; + } + + // 计算渲染位置 + Font font = mc.font; + int maxWidth = getMaxLineWidth(font, allLines); + int totalHeight = allLines.size() * font.lineHeight; + + // 自动调整位置以避免与其他调试信息重叠 + int x = width - MARGIN - maxWidth; + int y = findSuitableYPosition(height, totalHeight); + + // 绘制背景 + drawBackground(poseStack, x, y, maxWidth, totalHeight, font); + + // 绘制文本 + drawTextLines(gui, poseStack, font, allLines, x, y); + } + + private List getAllDisplayLines() { + List lines = new ArrayList<>(); + + // 添加Ping信息 + String pingText = ClientPingHandler.getPingDisplayText(); + if (!pingText.isEmpty()) { + lines.addAll(List.of(pingText.split("\n"))); + } + + // 添加统计信息 + String statsText = ClientPingHandler.getStatsDisplayText(); + if (!statsText.isEmpty()) { + if (!lines.isEmpty()) lines.add(""); // 添加分隔空行 + lines.addAll(List.of(statsText.split("\n"))); + } + + return lines; + } + + private int getMaxLineWidth(Font font, List lines) { + return lines.stream() + .mapToInt(font::width) + .max() + .orElse(0); + } + + private int findSuitableYPosition(int screenHeight, int totalHeight) { + // 基础位置(从顶部开始) + int baseY = 10; + + // 检查是否会与其他调试信息重叠 + // 这里可以根据需要添加更复杂的检测逻辑 + if (baseY + totalHeight > screenHeight / 2) { + // 如果会重叠,则移动到屏幕下半部分 + return screenHeight - totalHeight - 30; + } + return baseY; + } + + private void drawBackground(PoseStack poseStack, int x, int y, int width, int height, Font font) { + GuiComponent.fill(poseStack, + x - PADDING, y - PADDING, + x + width + PADDING, y + height + PADDING, + BACKGROUND_COLOR); + } + + private void drawTextLines(ForgeIngameGui gui, PoseStack poseStack, Font font, List lines, int x, int y) { + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (!line.isEmpty()) { + gui.getFont().draw(poseStack, line, + x, + y + i * font.lineHeight, + TEXT_COLOR); + } + } + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginCommand.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PluginCommand.java similarity index 87% rename from forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginCommand.java rename to forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PluginCommand.java index 8615343..7596da7 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/PluginCommand.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/client/overlay/PluginCommand.java @@ -1,4 +1,4 @@ -package com.leisuretimedock.crossmod.client; +package com.leisuretimedock.crossmod.client.overlay; import java.util.Arrays; import java.util.Optional; diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/command/PingCommand.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/command/PingCommand.java new file mode 100644 index 0000000..156f2af --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/command/PingCommand.java @@ -0,0 +1,275 @@ +package com.leisuretimedock.crossmod.command; + +import com.leisuretimedock.crossmod.network.NetworkHandler; +import com.leisuretimedock.crossmod.network.PingRequestManager; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.commands.arguments.EntityArgument; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.server.level.ServerPlayer; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +public class PingCommand { + public static void register(CommandDispatcher dispatcher) { + LiteralArgumentBuilder networkping = + Commands.literal("netping") + .requires(source -> source.hasPermission(2)) + .executes(context -> executeFullReport(context.getSource())) + + .then(Commands.literal("report") + .executes(context -> executeFullReport(context.getSource())) + .then(Commands.argument("players", EntityArgument.players()) + .executes(context -> executePlayerReport( + context.getSource(), + EntityArgument.getPlayers(context, "players") + ))) + ) + .then(Commands.literal("stats") + .executes(context -> executeStatsReport(context.getSource()))) + + .then(Commands.literal("ping") + .then(Commands.argument("players", EntityArgument.players()) + .executes(context -> executePingPlayers( + context.getSource(), + EntityArgument.getPlayers(context, "players") + )) + ) + .executes(context -> executeSinglePing(context.getSource()))) + + .then(Commands.literal("monitor") + .then(Commands.argument("players", EntityArgument.players()) + .executes(context -> executeToggleMonitoring( + context.getSource(), + EntityArgument.getPlayers(context, "players"), + true + )) + ) + .executes(context -> executeToggleMonitoring(context.getSource(), true))) + + .then(Commands.literal("unmonitor") + .then(Commands.argument("players", EntityArgument.players()) + .executes(context -> executeToggleMonitoring( + context.getSource(), + EntityArgument.getPlayers(context, "players"), + false + )) + ) + .executes(context -> executeToggleMonitoring(context.getSource(), false))) + + .then(Commands.literal("multiping") + .then(Commands.argument("players", EntityArgument.players()) + .then(Commands.argument("count", IntegerArgumentType.integer(1, PingRequestManager.getMAX_BATCH_PINGS())) + .then(Commands.argument("interval", IntegerArgumentType.integer((int) PingRequestManager.getMIN_PING_INTERVAL(), 5000)) + .executes(ctx -> executeMultiPing( + ctx.getSource(), + EntityArgument.getPlayers(ctx, "players"), + IntegerArgumentType.getInteger(ctx, "count"), + IntegerArgumentType.getInteger(ctx, "interval") + )) + ) + ) + .executes(ctx -> executeMultiPing( + ctx.getSource(), + Collections.singleton(ctx.getSource().getPlayerOrException()), + 5, // 默认次数 + 1000 // 默认间隔(ms) + )) + ) + ); + + dispatcher.register(networkping); + } + + private static int executePlayerReport(CommandSourceStack source, Collection players) throws CommandSyntaxException { + if (players.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.error.no_players"), false); + return 0; + } + + ServerPlayer requester = source.getPlayerOrException(); + Map results = PingRequestManager.getLatestPingsForPlayers(players); + + if (results.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.info.no_data"), false); + return Command.SINGLE_SUCCESS; + } + + NetworkHandler.sendPingResults(requester, results); + sendTextReport(requester, results); + + return Command.SINGLE_SUCCESS; + } + + private static int executeFullReport(CommandSourceStack source) throws CommandSyntaxException { + ServerPlayer player = source.getPlayerOrException(); + Map results = PingRequestManager.getAllLatestPings(); + + if (results.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.info.no_data"), false); + return Command.SINGLE_SUCCESS; + } + + NetworkHandler.sendPingResults(player, results); + sendTextReport(player, results); + + // 发送统计信息 + PingRequestManager.PingStats stats = PingRequestManager.getGlobalPingStats(); + NetworkHandler.sendPingStats(player, stats); + + return Command.SINGLE_SUCCESS; + } + + private static int executeStatsReport(CommandSourceStack source) throws CommandSyntaxException { + ServerPlayer player = source.getPlayerOrException(); + PingRequestManager.PingStats stats = PingRequestManager.getGlobalPingStats(); + + if (stats.sampleCount() == 0) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.info.no_data"), false); + return Command.SINGLE_SUCCESS; + } + + NetworkHandler.sendPingStats(player, stats); + sendStatsTextReport(player, stats); + + return Command.SINGLE_SUCCESS; + } + + private static int executeSinglePing(CommandSourceStack source) throws CommandSyntaxException { + ServerPlayer player = source.getPlayerOrException(); + if(!PingRequestManager.isMonitored(player.getUUID())) { + source.sendFailure(new TranslatableComponent("ltd.mod.ping.error.not_monitored.self")); + return -1; + } + PingRequestManager.ping(player); + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.success.ping_self"), false); + return Command.SINGLE_SUCCESS; + } + + private static int executePingPlayers(CommandSourceStack source, Collection players) throws CommandSyntaxException { + if (players.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.error.no_players"), false); + return 0; + } + + players.forEach(player -> { + if(!PingRequestManager.isMonitored(player.getUUID())) { + source.sendFailure(new TranslatableComponent("ltd.mod.ping.error.not_monitored.other", + player.getScoreboardName())); + } + else { + PingRequestManager.ping(player); + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.success.ping_other", + player.getScoreboardName()), false); + } + }); + + return Command.SINGLE_SUCCESS; + } + private static int executeMultiPing(CommandSourceStack source, + Collection players, + int count, + int interval) { + if (players.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.error.no_players"), false); + return 0; + } + + players.forEach(player -> { + if (PingRequestManager.sendMultiplePings(player, count, interval)) { + source.sendSuccess( + player.getScoreboardName().equals(source.getTextName()) ? + new TranslatableComponent("ltd.mod.ping.success.multiping.start.self", count, interval) : + new TranslatableComponent("ltd.mod.ping.success.multiping.start.other", player.getScoreboardName(), count, interval), + false); + } else { + source.sendFailure( + new TranslatableComponent( + player.getScoreboardName().equals(source.getTextName()) ? + "ltd.mod.ping.error.multiping.fail.self" : + "ltd.mod.ping.error.multiping.fail.other", + player.getScoreboardName() + ) + ); + } + }); + + return Command.SINGLE_SUCCESS; + } + private static int executeToggleMonitoring(CommandSourceStack source, boolean monitor) throws CommandSyntaxException { + ServerPlayer player = source.getPlayerOrException(); + if (monitor) { + PingRequestManager.monitor(player); + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.success.monitor.self"), false); + } else { + PingRequestManager.unmonitor(player); + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.success.unmonitor.self"), false); + } + return Command.SINGLE_SUCCESS; + } + + private static int executeToggleMonitoring(CommandSourceStack source, Collection players, boolean monitor) throws CommandSyntaxException { + if (players.isEmpty()) { + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.error.no_players"), false); + return 0; + } + + players.forEach(player -> { + if (monitor) { + PingRequestManager.monitor(player); + source.sendFailure(new TranslatableComponent("ltd.mod.ping.error.not_monitored.other", + player.getScoreboardName())); + } else { + PingRequestManager.unmonitor(player); + source.sendSuccess(new TranslatableComponent("ltd.mod.ping.success.ping_other", + player.getScoreboardName()), false); + } + }); + + return players.size(); + } + + private static void sendTextReport(ServerPlayer player, Map results) { + player.sendMessage(new TranslatableComponent("ltd.mod.ping.title.report").withStyle(ChatFormatting.GOLD), + player.getUUID()); + + results.forEach((uuid, ping) -> { + player.sendMessage( + new TranslatableComponent( + "ltd.mod.ping.report.entry", + uuid.toString().substring(0, 8), + ping, + PingRequestManager.getAverageLatency(uuid), + PingRequestManager.getPacketLossRate(uuid)), + player.getUUID() + ); + }); + } + + private static void sendStatsTextReport(ServerPlayer player, PingRequestManager.PingStats stats) { + player.sendMessage(new TranslatableComponent("ltd.mod.ping.title.stats").withStyle(ChatFormatting.GOLD), + player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.average", stats.average()), player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.max", stats.max()), player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.min", stats.max()), player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.avg_latency", stats.averageLatency()), player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.packet_loss", stats.packetLossRate()), player.getUUID()); + player.sendMessage(new TranslatableComponent( + "ltd.mod.ping.stats.sample_count", stats.sampleCount()), player.getUUID()); + + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/CrossServerModMixinPlugin.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/CrossServerModMixinPlugin.java new file mode 100644 index 0000000..93e0164 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/mixin/CrossServerModMixinPlugin.java @@ -0,0 +1,45 @@ +package com.leisuretimedock.crossmod.mixin; + +import net.minecraftforge.fml.loading.FMLEnvironment; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class CrossServerModMixinPlugin implements IMixinConfigPlugin { + @Override + public void onLoad(String s) { + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String s, String s1) { + return !FMLEnvironment.dist.isDedicatedServer(); + } + + @Override + public void acceptTargets(Set set, Set set1) { + + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String s, ClassNode classNode, String s1, IMixinInfo iMixinInfo) { + + } + + @Override + public void postApply(String s, ClassNode classNode, String s1, IMixinInfo iMixinInfo) { + + } +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/NetworkHandler.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/NetworkHandler.java new file mode 100644 index 0000000..db9fcdc --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/NetworkHandler.java @@ -0,0 +1,155 @@ +// 客户端网络处理类(CrossMod 端) +package com.leisuretimedock.crossmod.network; + +import com.leisuretimedock.crossmod.CrossTeleportMod; +import com.leisuretimedock.crossmod.network.toClient.PingMessagePayload; +import com.leisuretimedock.crossmod.network.toClient.PingResultPacket; +import com.leisuretimedock.crossmod.network.toClient.PingStatsPacket; +import com.leisuretimedock.crossmod.network.toServer.PongMessagePayload; +import io.netty.buffer.Unpooled; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.Minecraft; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.network.PacketDistributor; +import net.minecraftforge.network.simple.SimpleChannel; + +import java.util.*; + +/** + * NetworkHandler + * 目前只用 plugin message 方式进行通信。 + */ +@Slf4j +public class NetworkHandler { + + // 自定义插件消息通道标识 + public static final ResourceLocation CHANNEL_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "channel"); + private static final String PROTOCOL_VERSION = "1"; + public static SimpleChannel CHANNEL; + private static int messageId = 0; + + /** + * 注册网络通道和消息处理器 + */ + public static void register() { + // 支持双端注册,以便服务器端也能处理相关命令 + CHANNEL = NetworkRegistry.newSimpleChannel( + new ResourceLocation(CrossTeleportMod.MOD_ID, "main"), + () -> PROTOCOL_VERSION, + PROTOCOL_VERSION::equals, + PROTOCOL_VERSION::equals + ); + CHANNEL.registerMessage( + messageId++, + PongMessagePayload.class, + PongMessagePayload::encode, + PongMessagePayload::decode, + PongMessagePayload::handle, + Optional.of(NetworkDirection.PLAY_TO_SERVER) + ); + CHANNEL.registerMessage( + messageId++, + PingMessagePayload.class, + PingMessagePayload::encode, + PingMessagePayload::decode, + PingMessagePayload::handle, + Optional.of(NetworkDirection.PLAY_TO_CLIENT) + ); + CHANNEL.registerMessage( + messageId++, + PingResultPacket.class, + PingResultPacket::encode, + PingResultPacket::decode, + PingResultPacket::handle + ); + CHANNEL.registerMessage( + messageId++, + PingStatsPacket.class, + PingStatsPacket::encode, + PingStatsPacket::decode, + PingStatsPacket::handle + ); + } + // 新增发送报告方法 + public static void sendPingReport(ServerPlayer player, + Map pingResults, + Map averageLatencies, + PingRequestManager.PingStats globalStats) { + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), + new PingResultPacket(pingResults, averageLatencies)); + + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), + new PingStatsPacket(globalStats)); + } + + + public static void sendPingResults(ServerPlayer player, Map results) { + // 创建平均时延映射 + Map averageLatencies = new HashMap<>(); + + // 为每个结果获取平均时延 + results.forEach((uuid, ping) -> { + double avgLatency = PingRequestManager.getAverageLatency(uuid); + averageLatencies.put(uuid, avgLatency); + }); + + // 发送包含ping值和平均时延的数据包 + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), + new PingResultPacket(results, averageLatencies)); + } + + + public static void sendPingStats(ServerPlayer player, PingRequestManager.PingStats stats) { + CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), + new PingStatsPacket(stats)); + } + + /** + * 发送Ping请求 + * @param player 服务器玩家 + */ + public static void sendPingRequest(ServerPlayer player, UUID requestId) { + try { + CHANNEL.sendTo(new PingMessagePayload(requestId), + player.connection.getConnection(), + NetworkDirection.PLAY_TO_CLIENT); + } catch (Exception e) { + log.error("发送ping请求失败", e); + } + } + + + /** + * 发送自定义插件消息 + * @param subChannel 子通道标识 + * @param payload 负载数据(字节数组) + */ + public static void sendPluginMessage(ResourceLocation subChannel, byte[] payload) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(payload.length)); + buf.writeBytes(payload); + + // 获取当前连接并发送自定义负载包 + Objects.requireNonNull(Minecraft.getInstance().getConnection()) + .send(new ServerboundCustomPayloadPacket(subChannel, buf)); + } + + /** + * 发送客户端已准备好消息(示例方法,调用具体实现) + */ + public static void sendClientReady() { + PluginMessageListener.sendClientReady(); + } + + /** + * 发送传送请求到代理服务器 + * @param serverName 目标服务器名 + */ + public static void sendTeleportRequest(String serverName) { + PluginMessageListener.sendTeleport(serverName); + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PingRequestManager.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PingRequestManager.java new file mode 100644 index 0000000..1565cfc --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PingRequestManager.java @@ -0,0 +1,436 @@ +package com.leisuretimedock.crossmod.network; + +import com.leisuretimedock.crossmod.CrossTeleportMod; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraft.server.level.ServerPlayer; + +import java.util.*; +import java.util.concurrent.*; +@Slf4j +public final class PingRequestManager { + // 配置常量 + private static final long DEFAULT_TIMEOUT_MS = 5000; + private static final long CLEANUP_INTERVAL_MS = 60000; + private static final int MAX_PING_HISTORY = 1024; + @Getter + private static final int MAX_BATCH_PINGS = 1024; // 单次批量最大Ping数 + @Getter + private static final long MIN_PING_INTERVAL = 50; // 最小间隔时间(ms) + @Getter + private static final long PING_INTERVAL = 1000; // 系统间隔时间(ms) + // 线程安全的存储结构 + private static final ConcurrentMap playerData = new ConcurrentHashMap<>(); + private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); + // 添加定时任务调度器 + private static final ScheduledExecutorService pingScheduler = Executors.newSingleThreadScheduledExecutor(); + static { + // 定期清理过期请求 + scheduler.scheduleAtFixedRate( + PingRequestManager::cleanupExpiredRequests, + CLEANUP_INTERVAL_MS, CLEANUP_INTERVAL_MS, + TimeUnit.MILLISECONDS + ); + // Ping任务 (新增) + pingScheduler.scheduleAtFixedRate(() -> { + try { + pingAllMonitoredPlayers(); + } catch (Exception e) { + log.error("Ping任务执行失败", e); + } + }, 1000, PING_INTERVAL, TimeUnit.MILLISECONDS); + } + + private PingRequestManager() {} // 防止实例化 + + // ========== 公共API ========== + + /** + * 监控玩家并开始收集ping数据 + */ + public static void monitor(ServerPlayer player) { + Objects.requireNonNull(player, "Player cannot be null"); + playerData.putIfAbsent(player.getUUID(), new PlayerPingData()); + } + + /** + * 停止监控玩家并清理数据 + */ + public static void unmonitor(ServerPlayer player) { + Objects.requireNonNull(player, "Player cannot be null"); + playerData.remove(player.getUUID()); + } + + public static boolean isMonitored(UUID uuid) { + return playerData.containsKey(uuid); + } + + /** + * 发起ping请求 + */ + public static void ping(ServerPlayer player) { + Objects.requireNonNull(player, "Player cannot be null"); + + PlayerPingData data = playerData.get(player.getUUID()); + if (data == null) return; // 玩家未被监控 + + UUID requestId = UUID.randomUUID(); + long currentTime = System.currentTimeMillis(); + + synchronized (data) { + data.totalRequests++; + data.activeRequests.put(requestId, currentTime); + } + + NetworkHandler.sendPingRequest(player, requestId); + } + + /** + * 完成ping请求并计算延迟 + */ + public static void complete(ServerPlayer player, UUID requestId) { + Objects.requireNonNull(player, "Player cannot be null"); + Objects.requireNonNull(requestId, "Request ID cannot be null"); + + PlayerPingData data = playerData.get(player.getUUID()); + if (data == null) return; + + synchronized (data) { + Long startTime = data.activeRequests.remove(requestId); + if (startTime != null) { + long ping = System.currentTimeMillis() - startTime; + data.successfulRequests++; + + // 网络拥塞检测 + if (ping > DEFAULT_TIMEOUT_MS * 0.8) { + player.sendMessage(new TranslatableComponent("ltd.mod.ping.warn.network_latency"), + player.getUUID()); + } + + updatePingHistory(data, ping); + } + } + } + + /** + * 获取最新ping值 + */ + public static Optional getLatestPing(UUID playerId) { + PlayerPingData data = playerData.get(playerId); + return data != null && !data.pingHistory.isEmpty() + ? Optional.of(data.pingHistory.getLast()) + : Optional.empty(); + } + + /** + * 获取ping统计数据(包含丢包率) + */ + public static PingStats getPingStats(UUID playerId) { + PlayerPingData data = playerData.get(playerId); + if (data == null || data.pingHistory.isEmpty()) { + return PingStats.EMPTY; + } + + // 计算基本统计数据 + LongSummaryStatistics stats = data.pingHistory.stream() + .mapToLong(Long::longValue) + .summaryStatistics(); + + // 计算丢包率 + double packetLossRate = calculatePacketLossRate(data); + + return new PingStats( + stats.getAverage(), + stats.getMax(), + stats.getMin(), + (int) stats.getCount(), + calculateAverageLatency(data.pingHistory), + packetLossRate + ); + } + + /** + * 获取玩家的平均时延 + */ + public static double getAverageLatency(UUID playerId) { + PlayerPingData data = playerData.get(playerId); + return data != null ? calculateAverageLatency(data.pingHistory) : 0; + } + + /** + * 获取玩家的丢包率 + */ + public static double getPacketLossRate(UUID playerId) { + PlayerPingData data = playerData.get(playerId); + return data != null ? calculatePacketLossRate(data) : 0; + } + + /** + * 获取全局Ping统计数据(包含丢包率) + */ + public static PingStats getGlobalPingStats() { + List allPings = new ArrayList<>(); + int totalRequests = 0; + int successfulRequests = 0; + + for (PlayerPingData data : playerData.values()) { + synchronized (data) { + allPings.addAll(data.pingHistory); + totalRequests += data.totalRequests; + successfulRequests += data.successfulRequests; + } + } + + if (allPings.isEmpty()) { + return PingStats.EMPTY; + } + + LongSummaryStatistics stats = allPings.stream() + .mapToLong(Long::longValue) + .summaryStatistics(); + + // 计算全局丢包率 + double globalPacketLossRate = totalRequests > 0 + ? (1 - (double) successfulRequests / totalRequests) * 100 + : 0; + + return new PingStats( + stats.getAverage(), + stats.getMax(), + stats.getMin(), + (int) stats.getCount(), + calculateAverageLatency(allPings), + globalPacketLossRate + ); + } + + /** + * 获取所有玩家的最新ping值 + */ + public static Map getAllLatestPings() { + Map results = new HashMap<>(); + + playerData.forEach((uuid, data) -> { + synchronized (data) { + if (!data.pingHistory.isEmpty()) { + results.put(uuid, data.pingHistory.getLast()); + } + } + }); + + return results; + } + /** + * 获取指定玩家的最新ping数据 + * @param players 要查询的玩家集合 + * @return 包含玩家UUID和最新ping时间的Map + */ + public static Map getLatestPingsForPlayers(Collection players) { + Map results = new HashMap<>(); + + for (ServerPlayer player : players) { + UUID uuid = player.getUUID(); + PlayerPingData data = playerData.get(uuid); + + if (data != null && !data.pingHistory.isEmpty()) { + synchronized (data) { + results.put(uuid, data.pingHistory.getLast()); + } + } + } + + return results; + } + /** + * 批量发送Ping请求 + * @param player 目标玩家 + * @param count 要发送的Ping包数量(1-10) + * @param intervalMs 发送间隔(毫秒,最少100ms) + * @return 是否成功开始发送 + */ + public static boolean sendMultiplePings(ServerPlayer player, int count, long intervalMs) { + // 参数验证 + if (count < 1 || count > MAX_BATCH_PINGS || intervalMs < MIN_PING_INTERVAL) { + return false; + } + + PlayerPingData data = playerData.get(player.getUUID()); + if (data == null) return false; + + synchronized (data) { + // 检查现有请求量 + if (data.activeRequests.size() > MAX_BATCH_PINGS || data.batchInProgress > 0) { + return false; + } + + data.batchInProgress = count; + } + + // 开始发送 + for (int i = 0; i < count; i++) { + final int attempt = i + 1; + scheduler.schedule(() -> { + synchronized (data) { + if (data.batchInProgress == 0) return; // 已取消 + + UUID requestId = UUID.randomUUID(); + data.totalRequests++; + data.activeRequests.put(requestId, System.currentTimeMillis()); + NetworkHandler.sendPingRequest(player, requestId); + + if (attempt == count) { + data.batchInProgress = 0; + player.sendMessage( + new TranslatableComponent("ltd.mod.ping.success.multiping.complete", count), + player.getUUID()); + } + } + }, i * Math.max(intervalMs, MIN_PING_INTERVAL), TimeUnit.MILLISECONDS); + } + + return true; + } + + /** + * 取消该玩家的所有进行中的Ping请求 + */ + public static void cancelPings(ServerPlayer player) { + PlayerPingData data = playerData.get(player.getUUID()); + if (data != null) { + synchronized (data) { + data.activeRequests.clear(); + data.batchInProgress = 0; + } + } + } + + // ========== 内部方法 ========== + // 新增方法:Ping所有被监控玩家 + private static void pingAllMonitoredPlayers() { + playerData.keySet().forEach(uuid -> { + ServerPlayer player = CrossTeleportMod.CommonEvents.getPlayerByUUID(uuid); + if (player != null && player.isAlive()) { + ping(player); + + // 每10次Ping发送一次报告 + PlayerPingData data = playerData.get(uuid); + if (data != null && data.totalRequests % 10 == 0) { + sendReportToClient(player); + } + } + }); + } + + // 改进的发送报告方法 + private static void sendReportToClient(ServerPlayer player) { + Map latestPings = new HashMap<>(); + Map averages = new HashMap<>(); + + playerData.forEach((uuid, data) -> { + synchronized (data) { + if (!data.pingHistory.isEmpty()) { + latestPings.put(uuid, PingRequestManager.getLatestPing(uuid).orElse(-1L)); + averages.put(uuid, PingRequestManager.calculateAverageLatency(data.pingHistory)); + } + } + }); + + // 添加全局统计 + PingStats globalStats = getGlobalPingStats(); + NetworkHandler.sendPingReport(player, latestPings, averages, globalStats); + } + private static void updatePingHistory(PlayerPingData data, long ping) { + data.addPing(ping); + } + + private static void cleanupExpiredRequests() { + long currentTime = System.currentTimeMillis(); + + playerData.forEach((playerId, data) -> { + synchronized (data) { + data.activeRequests.entrySet().removeIf(entry -> + currentTime - entry.getValue() > DEFAULT_TIMEOUT_MS + ); + // 将超时的请求计入丢包统计 + int expiredCount = data.activeRequests.size(); + if (expiredCount > 0) { + data.totalRequests += expiredCount; + data.activeRequests.clear(); + } + } + }); + } + + + private static double calculateAverageLatency(Collection pingHistory) { + if (pingHistory.isEmpty()) return 0; + + double total = 0; + double weightSum = 0; + int i = 1; + + for (Long ping : pingHistory) { + double weight = 1.0 / i; + total += ping * weight; + weightSum += weight; + i++; + } + + return total / weightSum; + } + + private static double calculatePacketLossRate(PlayerPingData data) { + synchronized (data) { + if (data.totalRequests == 0) return 0; + return (1 - (double) data.successfulRequests / data.totalRequests) * 100; + } + } + + // ========== 数据结构 ========== + + private static class PlayerPingData { + final LinkedList pingHistory = new LinkedList<>(); + final Map activeRequests = new ConcurrentHashMap<>(); + volatile double currentLatency = 0; + int totalRequests = 0; + int successfulRequests = 0; + int batchInProgress = 0; + + synchronized void addPing(long ping) { + pingHistory.add(ping); + if (pingHistory.size() > MAX_PING_HISTORY) { + pingHistory.removeFirst(); + } + currentLatency = calculateAverageLatency(pingHistory); + } + + private double calculateAverageLatency(Collection pings) { + if (pings.isEmpty()) return 0; + + double total = 0; + double weightSum = 0; + int i = 1; + + for (Long ping : pings) { + double weight = 1.0 / i; + total += ping * weight; + weightSum += weight; + i++; + } + + return total / weightSum; + } + } + + public record PingStats( + double average, + long max, + long min, + int sampleCount, + double averageLatency, + double packetLossRate + ) { + public static final PingStats EMPTY = new PingStats(0, 0, 0, 0, 0, 0); + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/PluginMessageListener.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PluginMessageListener.java similarity index 97% rename from forge-mod/src/main/java/com/leisuretimedock/crossmod/PluginMessageListener.java rename to forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PluginMessageListener.java index 335e931..7b50e5a 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/PluginMessageListener.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/PluginMessageListener.java @@ -1,4 +1,4 @@ -package com.leisuretimedock.crossmod; +package com.leisuretimedock.crossmod.network; import lombok.extern.slf4j.Slf4j; import net.minecraft.client.Minecraft; diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingMessagePayload.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingMessagePayload.java new file mode 100644 index 0000000..3b07c50 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingMessagePayload.java @@ -0,0 +1,34 @@ +package com.leisuretimedock.crossmod.network.toClient; + +import com.leisuretimedock.crossmod.network.toServer.PongMessagePayload; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.network.NetworkEvent; + +import java.util.UUID; +import java.util.function.Supplier; + +import static com.leisuretimedock.crossmod.network.NetworkHandler.CHANNEL; + +//Server -> Client +public record PingMessagePayload(UUID requestId) { + public static void encode(PingMessagePayload payload, FriendlyByteBuf buf) { + buf.writeLong(payload.requestId().getMostSignificantBits()); + buf.writeLong(payload.requestId().getLeastSignificantBits()); + } + + public static PingMessagePayload decode(FriendlyByteBuf buf) { + long mostSignificantBits = buf.readLong(); + long leastSignificantBits = buf.readLong(); + return new PingMessagePayload(new UUID(mostSignificantBits, leastSignificantBits)); + } + //客户端处理 + public static void handle(PingMessagePayload msg, Supplier ctx) { + ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { + // 客户端收到ping请求,立即返回pong + CHANNEL.sendToServer(new PongMessagePayload(msg.requestId)); + })); + ctx.get().setPacketHandled(true); + } +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingResultPacket.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingResultPacket.java new file mode 100644 index 0000000..b2c3137 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingResultPacket.java @@ -0,0 +1,57 @@ +package com.leisuretimedock.crossmod.network.toClient; + +import com.leisuretimedock.crossmod.client.ClientPingHandler; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +public class PingResultPacket { + private final long timestamp; + private final Map pingResults; // UUID -> ping值 + private final Map averageLatencies; // UUID -> 平均时延 + + public PingResultPacket(Map pingResults, Map averageLatencies) { + this.timestamp = System.currentTimeMillis(); + this.pingResults = new HashMap<>(pingResults); + this.averageLatencies = new HashMap<>(averageLatencies); + } + public PingResultPacket(FriendlyByteBuf buf) { + this.timestamp = System.currentTimeMillis(); + int size = buf.readVarInt(); + this.pingResults = new HashMap<>(size); + this.averageLatencies = new HashMap<>(size); + for (int i = 0; i < size; i++) { + UUID uuid = buf.readUUID(); + long ping = buf.readLong(); + double avgLatency = buf.readDouble(); + pingResults.put(uuid, ping); + averageLatencies.put(uuid, avgLatency); + } + } + public static PingResultPacket decode(FriendlyByteBuf buf) { + return new PingResultPacket(buf); + } + + public void encode(FriendlyByteBuf buf) { + buf.writeVarInt(pingResults.size()); + pingResults.forEach((uuid, ping) -> { + buf.writeUUID(uuid); + buf.writeLong(ping); + buf.writeDouble(averageLatencies.getOrDefault(uuid, 0.0)); + }); + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + // 检查数据时效性(5秒内有效) + if (System.currentTimeMillis() - timestamp < 5000) { + ClientPingHandler.handlePingResults(pingResults, averageLatencies); + } + }); + ctx.get().setPacketHandled(true); + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingStatsPacket.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingStatsPacket.java new file mode 100644 index 0000000..14f012a --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/PingStatsPacket.java @@ -0,0 +1,46 @@ +package com.leisuretimedock.crossmod.network.toClient; + +import com.leisuretimedock.crossmod.client.ClientPingHandler; +import com.leisuretimedock.crossmod.network.PingRequestManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +public class PingStatsPacket { + + private final PingRequestManager.PingStats stats; + + public PingStatsPacket(PingRequestManager.PingStats stats) { + this.stats = stats; + } + + public static PingStatsPacket decode(FriendlyByteBuf buf) { + return new PingStatsPacket( + new PingRequestManager.PingStats( + buf.readDouble(), + buf.readLong(), + buf.readLong(), + buf.readInt(), + buf.readDouble(), + buf.readDouble() + ) + ); + } + + public void encode(FriendlyByteBuf buf) { + buf.writeDouble(stats.average()); + buf.writeLong(stats.max()); + buf.writeLong(stats.min()); + buf.writeInt(stats.sampleCount()); + buf.writeDouble(stats.averageLatency()); + buf.writeDouble(stats.packetLossRate()); + } + + public void handle(Supplier ctx) { + ctx.get().enqueueWork(() -> { + ClientPingHandler.handlePingStats(stats); + }); + ctx.get().setPacketHandled(true); + } +} \ No newline at end of file diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/ResetPacket.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/ResetPacket.java new file mode 100644 index 0000000..11937a5 --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toClient/ResetPacket.java @@ -0,0 +1,66 @@ +package com.leisuretimedock.crossmod.network.toClient; + +import com.leisuretimedock.crossmod.mixin.AccessorMinecraft; +import com.leisuretimedock.crossmod.reset.ClientResetManager; +import com.leisuretimedock.crossmod.reset.ResetHelper; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl; +import net.minecraft.network.Connection; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.TranslatableComponent; +import net.minecraftforge.network.*; + +import java.util.function.Supplier; + +@Slf4j +@Setter +@Getter +public class ResetPacket extends HandshakeMessages.C2SAcknowledge { + private int loginIndex; + public ResetPacket() { + super(); + } + public static ResetPacket decode(FriendlyByteBuf ignoredBuf) { + return new ResetPacket(); + } + + public void encode(FriendlyByteBuf buf) { + } + + public static void handler(HandshakeHandler ignoredHandler, ResetPacket ignoredMsg, Supplier ctxSupplier) { + NetworkEvent.Context ctx = ctxSupplier.get(); + ClientResetManager.isNegotiating.set(true); + Connection conn = ctx.getNetworkManager(); + if (ctx.getDirection() != NetworkDirection.LOGIN_TO_CLIENT && ctx.getDirection() != NetworkDirection.PLAY_TO_CLIENT) { + conn.disconnect(new TranslatableComponent("ltd.mod.client.invalid_packet")); + return; + } + if (ResetHelper.clearClient(ctx)) { + NetworkHooks.registerClientLoginChannel(conn); + conn.setProtocol(ConnectionProtocol.LOGIN); + conn.setListener(new ClientHandshakePacketListenerImpl( + conn, Minecraft.getInstance(), null, s -> {} + )); + + ((AccessorMinecraft) Minecraft.getInstance()).setPendingConnection(conn); + + try { + ClientResetManager.handshakeChannel.reply( + new HandshakeMessages.C2SAcknowledge(), + ClientResetManager.contextConstructor.newInstance(conn, NetworkDirection.LOGIN_TO_CLIENT, 98) + ); + } catch (Exception e) { + log.error("Failed to send acknowledgment", e); + conn.disconnect(new TranslatableComponent("ltd.mod.client.error.handshake")); + } + } + ctx.setPacketHandled(true); + + } + +} + diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toServer/PongMessagePayload.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toServer/PongMessagePayload.java new file mode 100644 index 0000000..025f3ec --- /dev/null +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/network/toServer/PongMessagePayload.java @@ -0,0 +1,29 @@ +package com.leisuretimedock.crossmod.network.toServer; + +import com.leisuretimedock.crossmod.network.PingRequestManager; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.UUID; +import java.util.function.Supplier; + +//Server +public record PongMessagePayload(UUID requestId) { + public static void encode(PongMessagePayload payload, FriendlyByteBuf buf) { + buf.writeLong(payload.requestId().getMostSignificantBits()); + buf.writeLong(payload.requestId().getLeastSignificantBits()); + } + + public static PongMessagePayload decode(FriendlyByteBuf buf) { + long mostSignificantBits = buf.readLong(); + long leastSignificantBits = buf.readLong(); + return new PongMessagePayload(new UUID(mostSignificantBits, leastSignificantBits)); + } + //服务器处理 + public static void handle(PongMessagePayload msg, Supplier ctx) { + ctx.get().enqueueWork(() -> { + PingRequestManager.complete(ctx.get().getSender(),msg.requestId); + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ClientResetManager.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ClientResetManager.java index 3de6f5d..6abb62e 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ClientResetManager.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ClientResetManager.java @@ -1,10 +1,16 @@ package com.leisuretimedock.crossmod.reset; +import com.leisuretimedock.crossmod.network.toClient.ResetPacket; import lombok.extern.slf4j.Slf4j; import net.minecraft.network.Connection; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.util.ObfuscationReflectionHelper; -import net.minecraftforge.network.*; +import net.minecraftforge.network.HandshakeHandler; +import net.minecraftforge.network.NetworkConstants; +import net.minecraftforge.network.NetworkDirection; +import net.minecraftforge.network.NetworkEvent; import net.minecraftforge.network.simple.SimpleChannel; import java.lang.reflect.Constructor; @@ -12,6 +18,7 @@ import java.lang.reflect.Field; import java.util.concurrent.atomic.AtomicBoolean; @Slf4j +@OnlyIn(Dist.CLIENT) public class ClientResetManager { public static final Field handshakeField; public static final Constructor contextConstructor; diff --git a/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ResetHelper.java b/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ResetHelper.java index ac22437..51fee79 100644 --- a/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ResetHelper.java +++ b/forge-mod/src/main/java/com/leisuretimedock/crossmod/reset/ResetHelper.java @@ -2,11 +2,8 @@ package com.leisuretimedock.crossmod.reset; import lombok.extern.slf4j.Slf4j; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.ConnectScreen; import net.minecraft.client.gui.screens.GenericDirtMessageScreen; import net.minecraft.client.multiplayer.ServerData; -import net.minecraft.client.multiplayer.resolver.ServerAddress; -import net.minecraft.network.chat.TextComponent; import net.minecraft.network.chat.TranslatableComponent; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; 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 71d2dfd..51a5ab4 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 @@ -1,9 +1,41 @@ { "ltd.mod.client.name.trans_server": "LTD Cross Server Mod", "ltd.mod.client.key": "Open LTD Cross Server Menu", + "ltd.mod.client.menu": "Cross Server Menu", + "ltd.mod.client.menu.button.1": "H Hub", + "ltd.mod.client.menu.button.2": "S Survival", + "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...", "ltd.mod.client.failed.reset_connection": "Failed to reset connection.", "ltd.mod.client.error.handshake": "Handshake error", "ltd.mod.client.invalid_reset_packet": "Invalid reset packet", - "ltd.mod.client.invalid_packet": "Invalid packet" + "ltd.mod.client.invalid_packet": "Invalid packet", + "ltd.mod.client.request.goto": "Request goto %s", + "ltd.mod.ping.error.no_players": "No valid players specified", + "ltd.mod.ping.info.no_data": "No valid ping data available", + "ltd.mod.ping.error.not_monitored.self": "You are not being monitored, please enable monitoring first", + "ltd.mod.ping.error.not_monitored.other": "Player %s is not being monitored, please enable monitoring first", + "ltd.mod.ping.success.ping_self": "Sent ping request to yourself", + "ltd.mod.ping.success.ping_other": "Sent ping request to player %s", + "ltd.mod.ping.success.monitor.self": "Started monitoring your ping", + "ltd.mod.ping.success.monitor.other": "Started monitoring ping for player %s", + "ltd.mod.ping.success.unmonitor.self": "Stopped monitoring your ping", + "ltd.mod.ping.success.unmonitor.other": "Stopped monitoring ping for player %s", + "ltd.mod.ping.success.multiping.start.self": "Started batch pinging yourself (%d times, %dms interval)", + "ltd.mod.ping.success.multiping.start.other": "Started batch pinging player %s (%d times, %dms interval)", + "ltd.mod.ping.success.multiping.complete": "Completed %d ping requests", + "ltd.mod.ping.error.multiping.fail.self": "Failed to start batch pinging yourself", + "ltd.mod.ping.error.multiping.fail.other": "Failed to start batch pinging player %s", + "ltd.mod.ping.title.report": "=== Network Latency Report ===", + "ltd.mod.ping.report.entry": "Player %s: %dms (Avg: %.1fms, Loss: %.1f%%)", + "ltd.mod.ping.title.stats": "=== Network Latency Statistics ===", + "ltd.mod.ping.stats.average": "Average latency: %.1fms", + "ltd.mod.ping.stats.max": "Maximum latency: %dms", + "ltd.mod.ping.stats.min": "Minimum latency: %dms", + "ltd.mod.ping.stats.avg_latency": "Average delay: %.1fms", + "ltd.mod.ping.stats.packet_loss": "Packet loss rate: %.1f%%", + "ltd.mod.ping.stats.sample_count": "Sample count: %d", + "ltd.mod.ping.warn.network_latency": "Network latency is high, so it is recommended to reduce the ping frequency" + } \ No newline at end of file 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 fd09310..b50665b 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 @@ -1,10 +1,40 @@ { "ltd.mod.client.name.trans_server": "LTD跨服传送模组", "ltd.mod.client.key": "打开LTD跨服传送菜单", + "ltd.mod.client.menu": "跨服菜单", + "ltd.mod.client.menu.button.1": "H 主城", + "ltd.mod.client.menu.button.2": "S 生存服", + "ltd.mod.client.menu.checkbox.show_trans_tip": "显示传送提示", + "ltd.mod.client.menu.checkbox.show_ping_stat": "显示Ping状态", "ltd.mod.client.negotiating": "重定向中 ...", "ltd.mod.client.failed.reset_connection": "重置链接失败。", "ltd.mod.client.error.handshake": "握手出错", "ltd.mod.client.invalid_reset_packet": "无效的重置链接包", - "ltd.mod.client.invalid_packet": "无效的包" - + "ltd.mod.client.invalid_packet": "无效的包", + "ltd.mod.client.request.goto": "请求去往 %s", + "ltd.mod.ping.error.no_players": "未指定有效玩家", + "ltd.mod.ping.info.no_data": "暂无有效的ping数据", + "ltd.mod.ping.error.not_monitored.self": "你未在ping监视中,请添加监视后再ping", + "ltd.mod.ping.error.not_monitored.other": "玩家%s未在ping监视中,请添加监视后再ping", + "ltd.mod.ping.success.ping_self": "已向你自己发送Ping请求", + "ltd.mod.ping.success.ping_other": "已向玩家%s发送Ping请求", + "ltd.mod.ping.success.monitor.self": "已开始监控你自己的ping", + "ltd.mod.ping.success.monitor.other": "已开始监控玩家%s的ping", + "ltd.mod.ping.success.unmonitor.self": "已停止监控你自己的ping", + "ltd.mod.ping.success.unmonitor.other": "已停止监控玩家%s的ping", + "ltd.mod.ping.success.multiping.start.self": "已开始批量Ping你自己 (%d次, 间隔%dms)", + "ltd.mod.ping.success.multiping.start.other": "已开始批量Ping玩家 %s (%d次, 间隔%dms)", + "ltd.mod.ping.success.multiping.complete": "已完成 %d 个Ping请求", + "ltd.mod.ping.error.multiping.fail.self": "无法开始批量Ping你自己", + "ltd.mod.ping.error.multiping.fail.other": "无法开始批量Ping玩家 %s", + "ltd.mod.ping.title.report": "=== 网络延迟报告 ===", + "ltd.mod.ping.report.entry": "玩家 %s: %dms (平均时延: %.1fms, 丢包率: %.1f%%)", + "ltd.mod.ping.title.stats": "=== 网络延迟统计 ===", + "ltd.mod.ping.stats.average": "平均延迟: %.1fms", + "ltd.mod.ping.stats.max": "最高延迟: %dms", + "ltd.mod.ping.stats.min": "最低延迟: %dms", + "ltd.mod.ping.stats.avg_latency": "平均时延: %.1fms", + "ltd.mod.ping.stats.packet_loss": "丢包率: %.1f%%", + "ltd.mod.ping.stats.sample_count": "样本数量: %d", + "ltd.mod.ping.warn.network_latency": "网络延迟较高,建议减少Ping频率" } \ No newline at end of file diff --git a/forge-mod/src/main/resources/ltdcrossteleport.mixins.json b/forge-mod/src/main/resources/ltdcrossteleport.mixins.json index ee2111c..8f8131a 100644 --- a/forge-mod/src/main/resources/ltdcrossteleport.mixins.json +++ b/forge-mod/src/main/resources/ltdcrossteleport.mixins.json @@ -1,11 +1,16 @@ { "required": true, "package": "com.leisuretimedock.crossmod.mixin", + "plugin": "com.leisuretimedock.crossmod.mixin.CrossServerModMixinPlugin", "compatibilityLevel": "JAVA_17", + "refmap": "ltdcrossteleport.refmap.json", "mixins": [ "AccessorMinecraft", "MixinMUINetWorkHandler", "ModListSpoofMixin" ], - "minVersion": "0.8" + "minVersion": "0.8", + "injectors": { + "defaultRequire": 1 + } } diff --git a/velocity-plugin/build.gradle b/velocity-plugin/build.gradle index 145b4d5..984c717 100644 --- a/velocity-plugin/build.gradle +++ b/velocity-plugin/build.gradle @@ -10,6 +10,7 @@ repositories { mavenCentral() maven { url 'https://repo.velocitypowered.com/releases/' } maven { url 'https://repo.lucko.me/' } // LuckPerms + maven { url "https://repo.william278.net/releases" } // } base { archivesName = plugin_name @@ -22,6 +23,7 @@ dependencies { implementation("org.spongepowered:configurate-yaml:4.1.2") annotationProcessor 'com.velocitypowered:velocity-api:3.2.0-SNAPSHOT' compileOnly 'net.luckperms:api:5.4' // LuckPerms API + implementation "net.william278:velocityscoreboardapi:1.1.3" } shadowJar { diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/CrossPlugin.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/CrossPlugin.java index 0607afc..0203b6d 100644 --- a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/CrossPlugin.java +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/CrossPlugin.java @@ -2,8 +2,10 @@ package com.leisuretimedock.crossplugin; import com.google.inject.Inject; import com.leisuretimedock.crossplugin.command.ReloadConfigCommand; +import com.leisuretimedock.crossplugin.listener.PingMessageListener; import com.leisuretimedock.crossplugin.listener.PluginMessageListener; import com.leisuretimedock.crossplugin.manager.ConfigManager; +import com.leisuretimedock.crossplugin.manager.PingManager; import com.leisuretimedock.crossplugin.messages.I18n; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; @@ -12,10 +14,13 @@ import com.velocitypowered.api.plugin.PluginContainer; import com.velocitypowered.api.plugin.annotation.DataDirectory; import com.velocitypowered.api.proxy.ProxyServer; import org.slf4j.Logger; +import org.slf4j.Marker; +import org.slf4j.MarkerFactory; import java.io.IOException; import java.nio.file.Path; import java.util.Locale; +import java.util.concurrent.TimeUnit; @Plugin( id = Static.PLUGIN_ID, @@ -24,21 +29,27 @@ import java.util.Locale; authors = "R3944Realms" ) public class CrossPlugin { - + public static final Marker CROSS_TELEPORT_MOD = MarkerFactory.getMarker("[CrossTeleportMod]"); private final ProxyServer server; public final Logger logger; - public final PluginMessageListener listener; + public final PluginMessageListener pluginMessageListener; + public final PingMessageListener pingMessageListener; public static boolean isLuckPermsEnabled; public final PluginContainer pluginContainer; + public final ConfigManager config; + public final PingManager pingManager; @Inject - public CrossPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory ,PluginContainer pluginContainer) throws IOException { + public CrossPlugin(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory , PluginContainer pluginContainer) throws IOException { this.server = server; this.logger = logger; - ConfigManager config = new ConfigManager(dataDirectory); + this.config = new ConfigManager(dataDirectory); + this.pingMessageListener = new PingMessageListener(config); + this.pingManager = new PingManager(server, config); + I18n.addBundle(Locale.US); I18n.addBundle(Locale.SIMPLIFIED_CHINESE); I18n.init(); - this.listener = new PluginMessageListener(server, logger, config); + this.pluginMessageListener = new PluginMessageListener(server, logger, config); this.pluginContainer = pluginContainer; server.getCommandManager().register( server.getCommandManager() @@ -53,10 +64,22 @@ public class CrossPlugin { @Subscribe public void onProxyInit(ProxyInitializeEvent event) { - server.getChannelRegistrar().register(PluginMessageListener.CHANNEL_ID, PluginMessageListener.TELEPORT_ID); - server.getEventManager().register(this, listener); + server.getChannelRegistrar().register( + PluginMessageListener.CHANNEL_ID, + PluginMessageListener.TELEPORT_ID, + PingMessageListener.PING_ID, + PingMessageListener.PONG_ID + ); + if (config.isEnablePing()) { + server.getEventManager().register(this, pluginMessageListener); + server.getEventManager().register(this, pingMessageListener); + server.getScheduler() + .buildTask(this, pingManager::measureAllBackendPing) + .repeat(config.getIntervalPing(), TimeUnit.SECONDS) + .schedule(); + } isLuckPermsEnabled = server.getPluginManager().getPlugin("luckperms").isPresent(); - logger.info("[INIT] Plugin initialized, channel registered."); + logger.info(CROSS_TELEPORT_MOD, "[INIT] Plugin initialized, channel registered."); } diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/command/PingCommand.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/command/PingCommand.java new file mode 100644 index 0000000..b6b1180 --- /dev/null +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/command/PingCommand.java @@ -0,0 +1,4 @@ +package com.leisuretimedock.crossplugin.command; + +public class PingCommand { +} diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PingMessageListener.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PingMessageListener.java new file mode 100644 index 0000000..069f7d7 --- /dev/null +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PingMessageListener.java @@ -0,0 +1,68 @@ +package com.leisuretimedock.crossplugin.listener; + +import com.leisuretimedock.crossplugin.Static; +import com.leisuretimedock.crossplugin.manager.ConfigManager; +import com.velocitypowered.api.event.Subscribe; +import com.velocitypowered.api.event.connection.PluginMessageEvent; +import com.velocitypowered.api.proxy.Player; +import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; +import lombok.extern.slf4j.Slf4j; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import static com.leisuretimedock.crossplugin.CrossPlugin.CROSS_TELEPORT_MOD; +//TODO: +// 1.模组C <-> 模组S +// 2.代理V <-> 模组S +// 3.模组C <-> 代理V +@Slf4j +public class PingMessageListener { + public static final MinecraftChannelIdentifier PING_ID = + MinecraftChannelIdentifier.create(Static.MOD_ID, "ping"); + public static final MinecraftChannelIdentifier PONG_ID = + MinecraftChannelIdentifier.create(Static.MOD_ID, "pong"); + private final ConfigManager configManager; + + public PingMessageListener(ConfigManager configManager) { + this.configManager = configManager; + } + + @Subscribe + public void onPluginMessageReceived(PluginMessageEvent event) { + if (!event.getIdentifier().equals(PING_ID)) return; + Player player = (Player) event.getTarget(); + handlePingChannel(player, event.getData()); + } + /** + * 处理“ping”子通道 + * @param player 玩家对象 + * @param data 消息字节数组 + */ //模组C <-> 代理V + private void handlePingChannel(Player player, byte[] data) { + if(configManager.isEnablePingLog()) log.debug(CROSS_TELEPORT_MOD, "Received ping msg from {}: {}", player.getUsername(), data); + try(DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) { + long clientTime = in.readLong(); + long velocityReceiveTime = System.currentTimeMillis(); + + // 准备响应 + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + DataOutputStream out = new DataOutputStream(baos); + out.writeLong(clientTime); // 客户端原始时间戳 + out.writeLong(velocityReceiveTime); // Velocity接收时间 + out.writeLong(System.currentTimeMillis()); // Velocity发送时间 + + byte[] response = baos.toByteArray(); + player.sendPluginMessage( + PONG_ID, + response + ); + } catch (Exception e) { + if(configManager.isEnableErrorLog())log.error(CROSS_TELEPORT_MOD, "failed to handle ping msg from {}: {}", player.getUsername(), e); + } + } + + +} diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PluginMessageListener.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PluginMessageListener.java index b2092ea..796a38d 100644 --- a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PluginMessageListener.java +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/listener/PluginMessageListener.java @@ -30,6 +30,8 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; +import static com.leisuretimedock.crossplugin.CrossPlugin.CROSS_TELEPORT_MOD; + /** * 插件消息监听器,负责接收客户端发来的插件消息并处理跨服传送、Overlay显示等逻辑。 @@ -60,7 +62,7 @@ public class PluginMessageListener { this.proxy = proxy; this.logger = logger; this.configManager = configManager; - this.serverManager = new ServerManager(proxy); + this.serverManager = new ServerManager(proxy, configManager); } /** @@ -87,7 +89,7 @@ public class PluginMessageListener { private void handleTeleportChannel(Player player, byte[] data) { // 跳过第一个 byte(长度信息),后面是 UTF-8 字符串 String raw = new String(data, 1, data.length - 1, StandardCharsets.UTF_8); - logger.debug("[CrossTeleportMod] Received teleport msg from {}: {}", player.getUsername(), raw); + if(configManager.isEnableListenerLog()) logger.debug(CROSS_TELEPORT_MOD, "Received teleport msg from {}: {}", player.getUsername(), raw); if (raw.startsWith("connect:")) { // 兼容旧的 connect: 方式,映射别名到真实服务器名 @@ -105,21 +107,23 @@ public class PluginMessageListener { * @param data 消息字节数组 */ private void handlePluginChannel(Player player, byte[] data) { - // 简单日志,打印字节长度和十六进制,便于调试 - log.trace("Received plugin message on channel 'channel' from player {}", player.getUsername()); - log.trace("Data length: {}", data.length); - StringBuilder sb = new StringBuilder(); - for (byte b : data) { - sb.append(String.format("%02X ", b)); + if(configManager.isEnableListenerLog()) { + // 简单日志,打印字节长度和十六进制,便于调试 + log.trace(CROSS_TELEPORT_MOD, "Received plugin message on channel 'channel' from player {}", player.getUsername()); + log.trace(CROSS_TELEPORT_MOD, "Data length: {}", data.length); + StringBuilder sb = new StringBuilder(); + for (byte b : data) { + sb.append(String.format("%02X ", b)); + } + log.trace("Data hex: {}", sb); } - log.trace("Data hex: {}", sb); try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(data))) { String command = in.readUTF(); - logger.debug("[CrossTeleportMod] Received plugin command from {}: {}", player.getUsername(), command); + if(configManager.isEnableListenerLog()) logger.debug(CROSS_TELEPORT_MOD, "Received plugin command from {}: {}", player.getUsername(), command); if ("client_ready".equals(command)) { if (waitingForReady.remove(player)) { - logger.debug("[CrossTeleportMod] {} is ready, sending overlay", player.getUsername()); + if(configManager.isEnableListenerLog()) logger.debug(CROSS_TELEPORT_MOD, " {} is ready, sending overlay", player.getUsername()); player.getCurrentServer().ifPresent(i -> { String name = i.getServerInfo().getName(); boolean contains = configManager.getOverlayServers().contains(name); @@ -130,17 +134,17 @@ public class PluginMessageListener { }); // TODO: 支持发送自定义服务器列表 } else { - logger.debug("[CrossTeleportMod] Received client_ready from {}, but not in waiting set", player.getUsername()); + if(configManager.isEnableListenerLog()) logger.debug(CROSS_TELEPORT_MOD, "Received client_ready from {}, but not in waiting set", player.getUsername()); } } else if (command.startsWith("teleport:")) { String server = command.substring("teleport:".length()); tryTeleport(player, server, true); } else { - logger.warn("[CrossTeleportMod] Unknown command: {}", command); + if(configManager.isEnableListenerLog()) logger.warn(CROSS_TELEPORT_MOD, "Unknown command: {}", command); } } catch (IOException e) { - logger.error("[CrossTeleportMod] Failed to parse plugin message from {}", player.getUsername(), e); + if(configManager.isEnableErrorLog()) logger.error(CROSS_TELEPORT_MOD, "Failed to parse plugin message from {}", player.getUsername(), e); } } @@ -164,7 +168,7 @@ public class PluginMessageListener { proxy.getServer(targetServer).ifPresentOrElse(server -> { player.createConnectionRequest(server).fireAndForget(); - logger.info("[CrossTeleportMod] Sent {} to {}", player.getUsername(), targetServer); + if (configManager.isEnableListenerLog()) logger.info(CROSS_TELEPORT_MOD, "Sent {} to {}", player.getUsername(), targetServer); }, () -> { player.sendMessage(I18n.translatable(I18nKeyEnum.SERVER_NOT_FOUND, NamedTextColor.RED, Component.text(targetServer))); @@ -179,7 +183,7 @@ public class PluginMessageListener { Player player = event.getPlayer(); String currentServer = event.getServer().getServerInfo().getName(); - logger.debug("[CrossTeleportMod] Player {} joined server {}", player.getUsername(), currentServer); + if(configManager.isEnableListenerLog()) logger.debug(CROSS_TELEPORT_MOD, "Player {} joined server {}", player.getUsername(), currentServer); waitingForReady.add(player); } diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ConfigManager.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ConfigManager.java index 86b728a..fc60515 100644 --- a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ConfigManager.java +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ConfigManager.java @@ -1,5 +1,7 @@ package com.leisuretimedock.crossplugin.manager; +import com.leisuretimedock.crossplugin.CrossPlugin; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.spongepowered.configurate.ConfigurationNode; import org.spongepowered.configurate.yaml.YamlConfigurationLoader; @@ -14,12 +16,31 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import static com.leisuretimedock.crossplugin.CrossPlugin.CROSS_TELEPORT_MOD; + @Slf4j public class ConfigManager { private final Path configPath; private final YamlConfigurationLoader loader; private final Map serverAliases = new ConcurrentHashMap<>(); private final Set overlayServers = ConcurrentHashMap.newKeySet(); + @Getter + private boolean enablePing; + @Getter + private long intervalPing; + @Getter + private boolean enableErrorLog = true; + @Getter + private boolean enableListenerLog = true; + @Getter + private boolean enablePingLog = true; + @Getter + private int goodThreshold; + @Getter + private int moderateThreshold; + @Getter + private int badThreshold; + private ConfigurationNode rootNode; public ConfigManager(Path configDir) throws IOException { @@ -66,11 +87,62 @@ public class ConfigManager { String name = node.getString(); if (name != null) overlayServers.add(name.toLowerCase()); } + // Load ping setting + ConfigurationNode pingSetting = rootNode.node("ping-setting"); + { + ConfigurationNode enable_ping = pingSetting.node("enable-ping"); + ConfigurationNode interval = pingSetting.node("interval"); - log.info("Loaded {} server aliases from config", serverAliases.size()); - log.info("Loaded {} overlay servers from config", overlayServers.size()); + if (pingSetting.virtual() || pingSetting.empty()) { + enable_ping.set(true); + interval.set(Long.class, 5); + loader.save(rootNode); + } + enablePing = enable_ping.getBoolean(true); + intervalPing = interval.getLong(5); + // 加载阈值配置 + ConfigurationNode thresholdsNode = rootNode.node("ping-setting", "thresholds"); + if (thresholdsNode.virtual() || thresholdsNode.empty()) { + // 设置默认阈值 + thresholdsNode.node("good").set(100); + thresholdsNode.node("moderate").set(200); + thresholdsNode.node("bad").set(300); + loader.save(rootNode); + } + + goodThreshold = thresholdsNode.node("good").getInt(100); + moderateThreshold = thresholdsNode.node("moderate").getInt(200); + badThreshold = thresholdsNode.node("bad").getInt(300); + + // 验证阈值有效性 + validateThresholds(); + + } + + // Load log setting + ConfigurationNode logInfoSetting = rootNode.node("log-info-setting"); + { + ConfigurationNode enable_error_log = logInfoSetting.node("enable-error-log"); + ConfigurationNode enable_listener_log = logInfoSetting.node("enable-listener-log"); + ConfigurationNode enable_ping_log = logInfoSetting.node("enable-ping-log"); + if (logInfoSetting.virtual() || logInfoSetting.empty()) { + enable_error_log.set(true); + enable_listener_log.set(true); + enable_ping_log.set(true); + loader.save(rootNode); + } + enableErrorLog = enable_error_log.getBoolean(true); + enableListenerLog = enable_listener_log.getBoolean(true); + enablePingLog = enable_ping_log.getBoolean(true); + } + + + + log.info(CROSS_TELEPORT_MOD, "Loaded {} server aliases from config", serverAliases.size()); + log.info(CROSS_TELEPORT_MOD, "Loaded {} overlay servers from config", overlayServers.size()); + log.info(CROSS_TELEPORT_MOD, "Loaded log setting from config ,error-log:{}, listener-log;{}, ping-log:{}", enableErrorLog, enableListenerLog, enablePingLog); } catch (IOException e) { - log.error("Failed to load configuration from {}", configPath, e); + log.error(CROSS_TELEPORT_MOD, "Failed to load configuration from {}", configPath, e); throw e; } } @@ -82,6 +154,16 @@ public class ConfigManager { load(); } + /** + * Validate Thresholds‘ valye + */ + private void validateThresholds() { + if (goodThreshold >= moderateThreshold || moderateThreshold >= badThreshold) { + throw new IllegalStateException("Invalid latency thresholds configuration: " + + "must be good < moderate < bad"); + } + } + /** * Saves the current configuration to disk */ @@ -143,7 +225,7 @@ public class ConfigManager { throw new IOException("Missing embedded config.yml in resources!"); } Files.copy(in, configPath); - log.info("Default config.yml copied to {}", configPath); + log.info(CROSS_TELEPORT_MOD, "Default config.yml copied to {}", configPath); } } } diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/PingManager.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/PingManager.java new file mode 100644 index 0000000..d74516d --- /dev/null +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/PingManager.java @@ -0,0 +1,60 @@ +package com.leisuretimedock.crossplugin.manager; + +import com.velocitypowered.api.proxy.ProxyServer; +import com.velocitypowered.api.proxy.server.RegisteredServer; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +import static com.leisuretimedock.crossplugin.CrossPlugin.CROSS_TELEPORT_MOD; + +@Slf4j +public class PingManager { + private final ProxyServer server; + private final Map serverPingResults = new HashMap<>(); + private final ConfigManager configManager; + + public PingManager(ProxyServer server, ConfigManager configManager) { + this.server = server; + this.configManager = configManager; + } + + /** + * 测量所有后端服务器的Ping值 + */ + public void measureAllBackendPing() { + server.getAllServers().forEach(serverInfo -> _measureBackendPing(serverInfo, System.currentTimeMillis())); + } + + /** + * 测量指定后端服务器的Ping值 + * @param backendName 后端服务器名称 + */ + public void measureBackendPing(String backendName) { + server.getAllServers().forEach(serverInfo -> { + if (serverInfo.getServerInfo().getName().equals(backendName)) { + _measureBackendPing(serverInfo, System.currentTimeMillis()); + } + }); + } + + /** + * 测量单个后端服务器的Ping值 + * @param serverInfo 后端服务器信息 + * @param startTime 开始时间 + */ + private void _measureBackendPing(RegisteredServer serverInfo, long startTime) { + serverInfo.ping().whenComplete((serverPing, throwable) -> { + long endTime = System.currentTimeMillis(); + long ping = endTime - startTime; + String name = serverInfo.getServerInfo().getName(); + if (throwable == null) { + serverPingResults.put(name, ping); + if(configManager.isEnablePingLog()) log.debug(CROSS_TELEPORT_MOD, "Ping to server {}: {}ms", name, ping); + } else { + if(configManager.isEnablePingLog()) log.warn(CROSS_TELEPORT_MOD, "Failed to ping server {}", name, throwable); + } + }); + } +} \ No newline at end of file diff --git a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ServerManager.java b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ServerManager.java index 5ae94f2..b33815b 100644 --- a/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ServerManager.java +++ b/velocity-plugin/src/main/java/com/leisuretimedock/crossplugin/manager/ServerManager.java @@ -11,10 +11,10 @@ import java.util.Optional; public class ServerManager { private final ProxyServer proxy; private final Map serverMap = new HashMap<>(); - - public ServerManager(ProxyServer proxy) { + private final ConfigManager configManager; + public ServerManager(ProxyServer proxy, ConfigManager configManager) { this.proxy = proxy; - + this.configManager = configManager; // 示例:静态初始化可跳转服务器 registerServer("lobby", "大厅服务器"); registerServer("survival", "生存服务器"); diff --git a/velocity-plugin/src/main/resources/config.yml b/velocity-plugin/src/main/resources/config.yml index 7798d91..09ee12c 100644 --- a/velocity-plugin/src/main/resources/config.yml +++ b/velocity-plugin/src/main/resources/config.yml @@ -19,3 +19,38 @@ server-aliases: # You can add multiple server names show-overlay-servers: - lobby +# ---------------------------------------- +# Ping Setting / 计算后端服务器、代理服务器与玩家之间的网络延迟 +ping-setting: + # 是否启用延迟计算机制 + # Enable or disable ping measurement + # 修改后需要重启代理才生效 + # Changes require a proxy restart to take effect + enable-ping: true + + # Ping测试间隔(秒) + # Ping measurement interval in seconds + # 修改后需要重启代理才生效 + # Changes require a proxy restart to take effect + interval: 5 + + # 延迟阈值配置(毫秒) + # Latency thresholds in milliseconds + thresholds: + good: 100 # 绿色显示 / Display in green + moderate: 200 # 黄色显示 / Display in yellow + bad: 300 # 红色显示 / Display in red +# ---------------------------------------- +# Log Settings / 日志记录设置 +log-info-setting: + # 是否记录错误日志 + # Enable error logging + enable-error-log: true + + # 是否记录监听器事件日志 + # Enable listener event logging + enable-listener-log: true + + # 是否记录Ping测试日志 + # Enable ping measurement logging + enable-ping-log: true \ No newline at end of file