添加了ping延迟功能
This commit is contained in:
parent
f5591e7df3
commit
6f045b681f
|
|
@ -1,4 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ASMIdeaPluginConfiguration">
|
||||
<asm skipDebug="false" skipFrames="false" skipCode="false" expandFrames="false" />
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID, PingData> pingData = new ConcurrentHashMap<>();
|
||||
private static PingRequestManager.PingStats lastStats;
|
||||
private static long lastStatsUpdateTime;
|
||||
|
||||
public static void handlePingResults(Map<UUID, Long> pingResults, Map<UUID, Double> 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<UUID, PingData> 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<String> getCombinedDebugText() {
|
||||
List<String> 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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CommandSourceStack> dispatcher) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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<String> 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<String> getAllDisplayLines() {
|
||||
List<String> 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<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.leisuretimedock.crossmod.client;
|
||||
package com.leisuretimedock.crossmod.client.overlay;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
|
@ -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<CommandSourceStack> dispatcher) {
|
||||
LiteralArgumentBuilder<CommandSourceStack> 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<ServerPlayer> players) throws CommandSyntaxException {
|
||||
if (players.isEmpty()) {
|
||||
source.sendSuccess(new TranslatableComponent("ltd.mod.ping.error.no_players"), false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
ServerPlayer requester = source.getPlayerOrException();
|
||||
Map<UUID, Long> 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<UUID, Long> 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<ServerPlayer> 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<ServerPlayer> 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<ServerPlayer> 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<UUID, Long> 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());
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String> set, Set<String> set1) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> 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) {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID, Long> pingResults,
|
||||
Map<UUID, Double> 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<UUID, Long> results) {
|
||||
// 创建平均时延映射
|
||||
Map<UUID, Double> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID, PlayerPingData> 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<Long> 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<Long> 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<UUID, Long> getAllLatestPings() {
|
||||
Map<UUID, Long> 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<UUID, Long> getLatestPingsForPlayers(Collection<ServerPlayer> players) {
|
||||
Map<UUID, Long> 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<UUID, Long> latestPings = new HashMap<>();
|
||||
Map<UUID, Double> 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<Long> 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<Long> pingHistory = new LinkedList<>();
|
||||
final Map<UUID, Long> 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<Long> 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.leisuretimedock.crossmod;
|
||||
package com.leisuretimedock.crossmod.network;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
|
@ -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<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
|
||||
// 客户端收到ping请求,立即返回pong
|
||||
CHANNEL.sendToServer(new PongMessagePayload(msg.requestId));
|
||||
}));
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<UUID, Long> pingResults; // UUID -> ping值
|
||||
private final Map<UUID, Double> averageLatencies; // UUID -> 平均时延
|
||||
|
||||
public PingResultPacket(Map<UUID, Long> pingResults, Map<UUID, Double> 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<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
// 检查数据时效性(5秒内有效)
|
||||
if (System.currentTimeMillis() - timestamp < 5000) {
|
||||
ClientPingHandler.handlePingResults(pingResults, averageLatencies);
|
||||
}
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
ClientPingHandler.handlePingStats(stats);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NetworkEvent.Context> 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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() -> {
|
||||
PingRequestManager.complete(ctx.get().getSender(),msg.requestId);
|
||||
});
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<NetworkEvent.Context> contextConstructor;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
@ -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频率"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package com.leisuretimedock.crossplugin.command;
|
||||
|
||||
public class PingCommand {
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String> serverAliases = new ConcurrentHashMap<>();
|
||||
private final Set<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, Long> 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -11,10 +11,10 @@ import java.util.Optional;
|
|||
public class ServerManager {
|
||||
private final ProxyServer proxy;
|
||||
private final Map<String, ServerInfo> 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", "生存服务器");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Reference in New Issue
Block a user