重构了些结构,并修复些BUG:

如Velocity简体中文翻译缺失
This commit is contained in:
叁玖领域 2025-07-22 17:06:33 +08:00
parent e8f0f81339
commit b4f87a7b55
17 changed files with 336 additions and 268 deletions

View File

@ -49,7 +49,7 @@ mod_name=Leisure Time Dock Mod
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=MIT
# The mod version. See https://semver.org/
mod_version=0.0.0.1
mod_version=0.0.0.2
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html

View File

@ -1,8 +1,6 @@
package com.leisuretimedock.crossmod;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.IExtensionPoint;
import net.minecraftforge.fml.ModLoadingContext;
@ -12,7 +10,7 @@ import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
@Mod(CrossTeleportMod.MOD_ID)
public class CrossTeleportMod {
public static final String MOD_ID ="ltdcrossteleport";
public static final ResourceLocation CHANNEL = new ResourceLocation(MOD_ID, "teleport");
public CrossTeleportMod() {
// 注册生命周期事件

View File

@ -1,64 +1,56 @@
// 客户端网络处理类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.ClientboundCustomPayloadPacket;
import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.network.NetworkRegistry;
import net.minecraftforge.network.simple.SimpleChannel;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Objects;
import static com.leisuretimedock.crossmod.client.PluginChannelClient.CHANNEL_ID;
/**
* NetworkHandler 用于客户端向服务端发送插件消息
* 目前只用 plugin message 方式进行通信
*/
public class NetworkHandler {
private static final String PROTOCOL_VERSION = "1";
private static SimpleChannel CHANNEL;
// 自定义插件消息通道标识
public static final ResourceLocation TELEPORT_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "teleport");
public static final ResourceLocation CHANNEL_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "channel");
public static void register() {
//TODO: 以后会做出双端版本以让游戏服务器端可以允运行代理命令简化些流程
// 不需要注册普通 packet因为我们只用 plugin message
CHANNEL = NetworkRegistry.newSimpleChannel(
new ResourceLocation(CrossTeleportMod.MOD_ID, "teleport"),
() -> PROTOCOL_VERSION,
PROTOCOL_VERSION::equals,
PROTOCOL_VERSION::equals
);
// TODO: 未来支持双端注册以便服务器端也能处理相关命令
// 当前仅客户端发送 PluginMessage无需额外注册
}
public static void sendTeleportMessage(String serverName) {
// 构建 raw plugin message
/**
* 发送自定义插件消息
* @param subChannel 子通道标识
* @param payload 负载数据字节数组
*/
public static void sendPluginMessage(ResourceLocation subChannel, byte[] payload) {
FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer());
buf.writeUtf(serverName);
// buf.writeUtf(subChannel.getPath()); // 写入子通道字符串
buf.writeBytes(payload); // 写入负载字节
Objects.requireNonNull(Minecraft.getInstance().getConnection()).send(
new ServerboundCustomPayloadPacket(
CrossTeleportMod.CHANNEL, buf
)
);
}
public static void sendClientReady() {
if (Minecraft.getInstance().player == null) return;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
try {
dos.writeUTF("client_ready");
dos.flush();
} catch (IOException e) {
throw new RuntimeException(e);
}
byte[] bytes = baos.toByteArray();
// 获取当前连接并发送自定义负载包
Objects.requireNonNull(Minecraft.getInstance().getConnection())
.send(new ServerboundCustomPayloadPacket(CHANNEL_ID, new FriendlyByteBuf(Unpooled.wrappedBuffer(bytes))));
.send(new ServerboundCustomPayloadPacket(subChannel, buf));
}
}
/**
* 发送客户端已准备好消息示例方法调用具体实现
*/
public static void sendClientReady() {
PluginMessageListener.sendClientReady();
}
/**
* 发送传送请求到代理服务器
* @param serverName 目标服务器名
*/
public static void sendTeleportRequest(String serverName) {
PluginMessageListener.sendTeleport(serverName);
}
}

View File

@ -0,0 +1,55 @@
package com.leisuretimedock.crossmod;
import lombok.extern.slf4j.Slf4j;
import net.minecraft.client.Minecraft;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
/**
* 客户端插件消息发送工具类负责向服务器发送自定义插件消息
*/
@Slf4j
public class PluginMessageListener {
/**
* 发送客户端已准备好消息给服务器CHANNEL_ID通道
*/
public static void sendClientReady() {
if (Minecraft.getInstance().player == null) return;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeUTF("client_ready"); // 命令字符串
dos.flush();
byte[] payload = baos.toByteArray();
NetworkHandler.sendPluginMessage(NetworkHandler.CHANNEL_ID, payload);
log.debug("Sent client_ready message with payload length: {}", payload.length);
} catch (IOException e) {
log.error("Failed to send client ready", e);
}
}
/**
* 发送传送请求给服务器TELEPORT_ID通道
* @param serverName 目标服务器名
*/
public static void sendTeleport(String serverName) {
if (Minecraft.getInstance().player == null) return;
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos)) {
// 旧协议写一个UTF字符串 teleport:目标服务器名
// 代理端代码是识别 "teleport:" 开头的字符串的
dos.writeUTF("teleport:" + serverName);
dos.flush();
NetworkHandler.sendPluginMessage(NetworkHandler.CHANNEL_ID, baos.toByteArray());
} catch (IOException e) {
log.error("Failed to send teleport", e);
}
}
}

View File

@ -13,7 +13,6 @@ 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.minecraft.resources.ResourceLocation;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ClientPlayerNetworkEvent;
import net.minecraftforge.client.event.RegisterClientCommandsEvent;
@ -24,25 +23,24 @@ import java.util.Objects;
@Slf4j
@Mod.EventBusSubscriber(modid = CrossTeleportMod.MOD_ID, value = Dist.CLIENT)
public class PluginChannelClient {
public static final ResourceLocation CHANNEL_ID = new ResourceLocation(CrossTeleportMod.MOD_ID, "channel");
private static final String HANDLER_NAME = CrossTeleportMod.MOD_ID+":channel";
private static final String HANDLER_NAME = CrossTeleportMod.MOD_ID + ":channel";
@SubscribeEvent
public static void onLogin(ClientPlayerNetworkEvent.LoggedInEvent event) {
log.info("[CrossTeleportMod] 玩家登录事件触发");
log.debug("[CrossTeleportMod] 玩家登录事件触发");
Connection connection = Objects.requireNonNull(Minecraft.getInstance().getConnection()).getConnection();
ChannelPipeline pipeline = connection.channel().pipeline();
log.info("[CrossTeleportMod] 当前管线内容: {}", pipeline.names());
log.debug("[CrossTeleportMod] 当前管线内容: {}", pipeline.names());
if (pipeline.get(HANDLER_NAME) == null) {
pipeline.addBefore("packet_handler", HANDLER_NAME, new SimpleChannelInboundHandler<ClientboundCustomPayloadPacket>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ClientboundCustomPayloadPacket packet) {
log.info("[CrossTeleportMod] 收到插件消息包: {}", packet.getIdentifier());
log.debug("[CrossTeleportMod] 收到插件消息包: {}", packet.getIdentifier());
if (!packet.getIdentifier().equals(CHANNEL_ID)) {
if (!packet.getIdentifier().equals(NetworkHandler.CHANNEL_ID)) {
log.warn("[CrossTeleportMod] 未识别插件消息频道: {}", packet.getIdentifier());
return;
}
@ -55,17 +53,17 @@ public class PluginChannelClient {
// 再读
String command = buf.readUtf();
log.info("[CrossTeleportMod] 收到指令: {}", command);
log.debug("[CrossTeleportMod] 收到指令: {}", command);
Minecraft.getInstance().execute(() -> {
PluginCommand.fromId(command).ifPresentOrElse(cmd -> {
switch (cmd) {
case OVERLAY_SHOW -> {
log.info("[CrossTeleportMod] 执行 OVERLAY_SHOW");
log.debug("[CrossTeleportMod] 执行 OVERLAY_SHOW");
OverlayRenderer.setShow(true);
}
case OVERLAY_HIDE -> {
log.info("[CrossTeleportMod] 执行 OVERLAY_HIDE");
log.debug("[CrossTeleportMod] 执行 OVERLAY_HIDE");
OverlayRenderer.setShow(false);
}
}
@ -78,7 +76,7 @@ public class PluginChannelClient {
}
});
log.info("[CrossTeleportMod] 已添加插件消息处理器: {}", HANDLER_NAME);
log.debug("[CrossTeleportMod] 已添加插件消息处理器: {}", HANDLER_NAME);
NetworkHandler.sendClientReady();
}
else {
@ -90,17 +88,17 @@ public class PluginChannelClient {
@SubscribeEvent
public static void onLogout(ClientPlayerNetworkEvent.LoggedOutEvent event) {
log.info("[CrossTeleportMod] 玩家注销事件触发");
log.debug("[CrossTeleportMod] 玩家注销事件触发");
Connection connection = event.getConnection();
if (connection != null) {
ChannelPipeline pipeline = connection.channel().pipeline();
log.info("[CrossTeleportMod] 当前管线内容: {}", pipeline.names());
log.debug("[CrossTeleportMod] 当前管线内容: {}", pipeline.names());
if (pipeline.get(HANDLER_NAME) != null) {
pipeline.remove(HANDLER_NAME);
log.info("[CrossTeleportMod] 成功移除插件消息处理器: {}", HANDLER_NAME);
log.debug("[CrossTeleportMod] 成功移除插件消息处理器: {}", HANDLER_NAME);
} else {
log.warn("[CrossTeleportMod] 未找到插件消息处理器: {}", HANDLER_NAME);
}
@ -116,7 +114,7 @@ public class PluginChannelClient {
.then(Commands.argument("server", StringArgumentType.string())
.executes(ctx -> {
String server = StringArgumentType.getString(ctx, "server");
NetworkHandler.sendTeleportMessage(server);
NetworkHandler.sendTeleportRequest(server);
ctx.getSource().sendSuccess(
new TextComponent("请求传送到 " + server), false);
return 1;

View File

@ -53,7 +53,7 @@ versionRange="${forge_version_range}" #mandatory
# AFTER - This mod is loaded AFTER the dependency
ordering="NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side="BOTH"
side="CLIENT"
# Here's another dependency
[[dependencies.${mod_id}]]
modId="minecraft"

View File

@ -1,4 +1,4 @@
{
"ltd.mod.client.name.trans_server": "LTD跨服传送模组",
"ltd.mod.client.key": "LTD跨服传送按键"
"ltd.mod.client.name.trans_server": "LTD Cross Server Mod",
"ltd.mod.client.key": "LTD Key"
}

View File

@ -1,3 +1,4 @@
{
"ltd.mod.client.name.trans_server": "LTD跨服传送模组",
"ltd.mod.client.key": "LTD跨服传送按键"
}

View File

@ -11,6 +11,9 @@ repositories {
maven { url 'https://repo.velocitypowered.com/releases/' }
maven { url 'https://repo.lucko.me/' } // LuckPerms
}
base {
archivesName = plugin_name
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.24'
@ -27,7 +30,7 @@ shadowJar {
jar {
manifest {
attributes 'Main-Class': 'com.yourname.CrossServerVelocityPlugin'
attributes 'Main-Class': 'com.leisuretimedock.crossplugin.CrossPlugin'
}
}
processResources{

View File

@ -1,2 +1,3 @@
plugin_group=com.leisuretimedock.crossplugin
plugin_version=1.0.0.0
plugin_version=1.0.0.2
plugin_name=CrossServerTeleport

View File

@ -2,16 +2,13 @@ package com.leisuretimedock.crossplugin;
import com.google.inject.Inject;
import com.leisuretimedock.crossplugin.command.ReloadConfigCommand;
import com.leisuretimedock.crossplugin.handler.PluginChannelHandler;
import com.leisuretimedock.crossplugin.handler.PluginMessageHandler;
import com.leisuretimedock.crossplugin.listener.PluginMessageListener;
import com.leisuretimedock.crossplugin.manager.ConfigManager;
import com.leisuretimedock.crossplugin.messages.I18n;
import com.mojang.brigadier.tree.CommandNode;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.PluginContainer;
import com.velocitypowered.api.plugin.PluginManager;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.ProxyServer;
import org.slf4j.Logger;
@ -30,8 +27,7 @@ public class CrossPlugin {
private final ProxyServer server;
public final Logger logger;
public final PluginMessageHandler pluginMessageHandler;
public final PluginChannelHandler pluginChannelHandler;
public final PluginMessageListener listener;
public static boolean isLuckPermsEnabled;
public final PluginContainer pluginContainer;
@Inject
@ -42,8 +38,7 @@ public class CrossPlugin {
I18n.addBundle(Locale.US);
I18n.addBundle(Locale.SIMPLIFIED_CHINESE);
I18n.init();
pluginChannelHandler = new PluginChannelHandler(server, logger, config);
pluginMessageHandler = new PluginMessageHandler(server, logger, config);
this.listener = new PluginMessageListener(server, logger, config);
this.pluginContainer = pluginContainer;
server.getCommandManager().register(
server.getCommandManager()
@ -58,9 +53,8 @@ public class CrossPlugin {
@Subscribe
public void onProxyInit(ProxyInitializeEvent event) {
server.getChannelRegistrar().register(PluginMessageHandler.CHANNEL_ID, PluginChannelHandler.CHANNEL_ID);
server.getEventManager().register(this, pluginChannelHandler);
server.getEventManager().register(this, pluginMessageHandler);
server.getChannelRegistrar().register(PluginMessageListener.CHANNEL_ID, PluginMessageListener.TELEPORT_ID);
server.getEventManager().register(this, listener);
isLuckPermsEnabled = server.getPluginManager().getPlugin("luckperms").isPresent();
logger.info("[INIT] Plugin initialized, channel registered.");
}

View File

@ -10,6 +10,9 @@ import lombok.extern.slf4j.Slf4j;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Slf4j
public class ReloadConfigCommand implements SimpleCommand {
@ -19,21 +22,24 @@ public class ReloadConfigCommand implements SimpleCommand {
public ReloadConfigCommand(ConfigManager configManager) {
this.configManager = configManager;
}
public static List<String> SUGGESTIONS = List.of("reload", "help");
@Override
public void execute(SimpleCommand.Invocation invocation) {
CommandSource source = invocation.source();
String[] args = invocation.arguments();
// /ltdcrossserver
// ltdcrossserver
if (args.length == 0) {
source.sendMessage(I18n.translatable(PERMISSION_HELP, NamedTextColor.YELLOW));
source.sendMessage(I18n.translatable(I18nKeyEnum.COMMAND_HELP, NamedTextColor.YELLOW));
return;
}
String subCommand = args[0].toLowerCase();
switch (subCommand) {
case "reload" -> handleReload(source);
default -> source.sendMessage(I18n.translatable(I18nKeyEnum.UNKNOWN_COMMAND, NamedTextColor.YELLOW));
case "help" -> source.sendMessage(I18n.translatable(I18nKeyEnum.COMMAND_HELP, NamedTextColor.YELLOW));
default -> source.sendMessage(I18n.translatable(I18nKeyEnum.UNKNOWN_COMMAND, NamedTextColor.YELLOW, Component.text(subCommand)));
}
@ -54,5 +60,11 @@ public class ReloadConfigCommand implements SimpleCommand {
log.error("Failed to reload config", e);
}
}
@Override
public CompletableFuture<List<String>> suggestAsync(Invocation invocation) {
return CompletableFuture.completedFuture(SUGGESTIONS);
}
}

View File

@ -1,95 +0,0 @@
package com.leisuretimedock.crossplugin.handler;
import com.leisuretimedock.crossplugin.Static;
import com.leisuretimedock.crossplugin.manager.ConfigManager;
import com.leisuretimedock.crossplugin.manager.OverlayManager;
import com.leisuretimedock.crossplugin.manager.ServerManager;
import com.leisuretimedock.crossplugin.messages.I18n;
import com.leisuretimedock.crossplugin.messages.I18nKeyEnum;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.slf4j.Logger;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class PluginChannelHandler {
public static final MinecraftChannelIdentifier CHANNEL_ID =
MinecraftChannelIdentifier.create(Static.MOD_ID, "channel");
private final ProxyServer proxy;
private final Logger logger;
private final ConfigManager configManager;
private final ServerManager serverManager;
private final Set<Player> waitingForReady = Collections.synchronizedSet(new HashSet<>());
public PluginChannelHandler(ProxyServer proxy, Logger logger, ConfigManager configManager) {
this.proxy = proxy;
this.logger = logger;
this.configManager = configManager;
this.serverManager = new ServerManager(proxy);
}
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!event.getIdentifier().equals(CHANNEL_ID)) return;
if (!(event.getSource() instanceof Player player)) return;
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(event.getData()))) {
String command = in.readUTF();
logger.debug("Received plugin message from {}: {}", player.getUsername(), command);
if (command.startsWith("teleport:")) {
String targetServer = command.substring("teleport:".length());
proxy.getServer(targetServer).ifPresentOrElse(server -> {
player.createConnectionRequest(server).fireAndForget();
logger.debug("Teleporting {} to {}", player.getUsername(), targetServer);
}, () -> {
player.sendMessage(I18n.translatable(I18nKeyEnum.SERVER_NOT_FOUND, NamedTextColor.RED, Component.text(targetServer)));
});
} else if ("client_ready".equals(command)) {
// 收到客户端准备消息
if (waitingForReady.remove(player)) {
logger.debug("[CrossTeleportMod] {} is ready, sending overlay and server list", player.getUsername());
OverlayManager.showOverlay(player);
//TODO未来计划使对应客户端mod可加载来自插件的自定义服务器列表
// OverlayManager.sendServerList(player, serverManager.getAvailableServers());
} else {
logger.debug("[CrossTeleportMod] Received client_ready from {}, but was not waiting", player.getUsername());
}
} else {
logger.warn("[CrossTeleportMod] Unknown plugin command from {}: {}", player.getUsername(), command);
}
} catch (IOException e) {
logger.error("[CrossTeleportMod] Error parsing plugin message", e);
}
}
@Subscribe
public void onPlayerJoin(ServerConnectedEvent event) {
Player player = event.getPlayer();
String currentServer = event.getServer().getServerInfo().getName();
logger.debug("[CrossTeleportMod] Player {} joined server {}", player.getUsername(), currentServer);
if (configManager.getOverlayServers().contains(currentServer)) {
// 标记此玩家等待客户端准备确认
waitingForReady.add(player);
logger.debug("[CrossTeleportMod] Added {} to waitingForReady set", player.getUsername());
} else {
// 不是 lobby隐藏 overlay
OverlayManager.hideOverlay(player);
logger.debug("[CrossTeleportMod] Hide overlay for player {}", player.getUsername());
}
}
}

View File

@ -1,85 +0,0 @@
package com.leisuretimedock.crossplugin.handler;
import com.leisuretimedock.crossplugin.CrossPlugin;
import com.leisuretimedock.crossplugin.Static;
import com.leisuretimedock.crossplugin.manager.ConfigManager;
import com.leisuretimedock.crossplugin.messages.I18n;
import com.leisuretimedock.crossplugin.messages.I18nKeyEnum;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.slf4j.Logger;
import java.nio.charset.StandardCharsets;
public class PluginMessageHandler {
public static final MinecraftChannelIdentifier CHANNEL_ID =
MinecraftChannelIdentifier.create(Static.MOD_ID, "teleport");
private static final String PERMISSION_HEAD = Static.MOD_ID + ".goto.";
private final ProxyServer server;
private final Logger logger;
private final ConfigManager config;
public PluginMessageHandler(ProxyServer server, Logger logger, ConfigManager config) {
this.server = server;
this.logger = logger;
this.config = config;
}
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!(event.getSource() instanceof Player player)) return;
if (!event.getIdentifier().equals(CHANNEL_ID)) return;
byte[] data = event.getData();
String raw = new String(data, 1, data.length - 1, StandardCharsets.UTF_8);
logger.info("Received plugin message from {}: {}", player.getUsername(), raw);
// 处理 connect:key 模式
if (raw.startsWith("connect:")) {
String key = raw.substring("connect:".length());
String targetServerName = config.resolveServerName(key);
if (isAlreadyOnServer(player, targetServerName)) {
player.sendMessage(I18n.translatable(I18nKeyEnum.ALREADY_ON_SERVER, NamedTextColor.RED));
return;
}
server.getServer(targetServerName).ifPresentOrElse(
srv -> player.createConnectionRequest(srv).fireAndForget(),
() -> player.sendMessage(I18n.translatable(I18nKeyEnum.SERVER_NOT_FOUND, NamedTextColor.RED, Component.text(targetServerName)))
);
return;
}
// 普通 serverName 模式
String permissionNode = PERMISSION_HEAD + raw;
//这个权限是 "ltdcrossteleport.goto.<xx服务器名>"
if (CrossPlugin.isLuckPermsEnabled && !player.hasPermission(permissionNode)) {
player.sendMessage(I18n.translatable(I18nKeyEnum.NO_PERMISSION_TO_TRANS_THIS_SERVER, NamedTextColor.RED, Component.text(raw)));
return;
}
if (isAlreadyOnServer(player, raw)) {
player.sendMessage(I18n.translatable(I18nKeyEnum.ALREADY_ON_SERVER, NamedTextColor.RED));
return;
}
server.getServer(raw).ifPresentOrElse(
srv -> player.createConnectionRequest(srv).fireAndForget(),
() -> player.sendMessage(I18n.translatable(I18nKeyEnum.SERVER_NOT_FOUND, NamedTextColor.RED, Component.text(raw)))
);
}
private boolean isAlreadyOnServer(Player player, String serverName) {
return player.getCurrentServer()
.map(current -> current.getServerInfo().getName().equalsIgnoreCase(serverName))
.orElse(false);
}
}

View File

@ -0,0 +1,194 @@
// 代理端插件消息监听器Velocity Proxy
package com.leisuretimedock.crossplugin.listener;
import com.leisuretimedock.crossplugin.Static;
import com.leisuretimedock.crossplugin.manager.ConfigManager;
import com.leisuretimedock.crossplugin.manager.OverlayManager;
import com.leisuretimedock.crossplugin.manager.ServerManager;
import com.leisuretimedock.crossplugin.messages.I18n;
import com.leisuretimedock.crossplugin.messages.I18nKeyEnum;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.connection.PluginMessageEvent;
import com.velocitypowered.api.event.player.ServerConnectedEvent;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.slf4j.Logger;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* 插件消息监听器负责接收客户端发来的插件消息并处理跨服传送Overlay显示等逻辑
*/
public class PluginMessageListener {
// 插件消息通道标识与客户端保持一致
public static final MinecraftChannelIdentifier TELEPORT_ID =
MinecraftChannelIdentifier.create(Static.MOD_ID, "teleport");
public static final MinecraftChannelIdentifier CHANNEL_ID =
MinecraftChannelIdentifier.create(Static.MOD_ID, "channel");
private static final String PERMISSION_HEAD = Static.MOD_ID + ".goto.";
private final ProxyServer proxy;
private final Logger logger;
private final ConfigManager configManager;
@SuppressWarnings("ALL")
private final ServerManager serverManager;
/**
* 维护等待客户端发送 "client_ready" 的玩家集合
*/
private final Set<Player> waitingForReady = Collections.synchronizedSet(new HashSet<>());
public PluginMessageListener(ProxyServer proxy, Logger logger, ConfigManager configManager) {
this.proxy = proxy;
this.logger = logger;
this.configManager = configManager;
this.serverManager = new ServerManager(proxy);
}
/**
* 监听插件消息事件
*/
@Subscribe
public void onPluginMessage(PluginMessageEvent event) {
if (!(event.getSource() instanceof Player player)) return;
MinecraftChannelIdentifier id = (MinecraftChannelIdentifier) event.getIdentifier();
if (id.equals(TELEPORT_ID)) {
handleTeleportChannel(player, event.getData());
} else if (id.equals(CHANNEL_ID)) {
handlePluginChannel(player, event.getData());
}
}
/**
* 处理teleport子通道旧协议兼容纯字符串形式
* @param player 玩家对象
* @param data 消息字节数组
*/
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 (raw.startsWith("connect:")) {
// 兼容旧的 connect: 方式映射别名到真实服务器名
String key = raw.substring("connect:".length());
String targetServerName = configManager.resolveServerName(key);
tryTeleport(player, targetServerName, false);
} else {
tryTeleport(player, raw, true);
}
}
/**
* 处理channel子通道支持多命令格式
* @param player 玩家对象
* @param data 消息字节数组
*/
private void handlePluginChannel(Player player, byte[] data) {
// 简单日志打印字节长度和十六进制便于调试
System.out.println("Received plugin message on channel 'channel' from player " + player.getUsername());
System.out.println("Data length: " + data.length);
StringBuilder sb = new StringBuilder();
for (byte b : data) {
sb.append(String.format("%02X ", b));
}
System.out.println("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 ("client_ready".equals(command)) {
if (waitingForReady.remove(player)) {
logger.debug("[CrossTeleportMod] {} is ready, sending overlay", player.getUsername());
OverlayManager.showOverlay(player);
// TODO: 支持发送自定义服务器列表
} else {
logger.debug("[CrossTeleportMod] 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);
}
} catch (IOException e) {
logger.error("[CrossTeleportMod] Failed to parse plugin message from {}", player.getUsername(), e);
}
}
/**
* 尝试传送玩家到目标服务器包含权限与当前所在服务器判断
* @param player 玩家对象
* @param targetServer 目标服务器名
* @param checkPermission 是否检查权限
*/
private void tryTeleport(Player player, String targetServer, boolean checkPermission) {
if (checkPermission && !player.hasPermission(PERMISSION_HEAD + targetServer)) {
player.sendMessage(I18n.translatable(I18nKeyEnum.NO_PERMISSION_TO_TRANS_THIS_SERVER,
NamedTextColor.RED, Component.text(targetServer)));
return;
}
if (isAlreadyOnServer(player, targetServer)) {
player.sendMessage(I18n.translatable(I18nKeyEnum.ALREADY_ON_SERVER, NamedTextColor.RED));
return;
}
proxy.getServer(targetServer).ifPresentOrElse(server -> {
player.createConnectionRequest(server).fireAndForget();
logger.info("[CrossTeleportMod] Sent {} to {}", player.getUsername(), targetServer);
}, () -> {
player.sendMessage(I18n.translatable(I18nKeyEnum.SERVER_NOT_FOUND,
NamedTextColor.RED, Component.text(targetServer)));
});
}
/**
* 监听玩家服务器连接事件维护是否显示 Overlay 状态
*/
@Subscribe
public void onPlayerJoin(ServerConnectedEvent event) {
Player player = event.getPlayer();
String currentServer = event.getServer().getServerInfo().getName();
logger.debug("[CrossTeleportMod] Player {} joined server {}", player.getUsername(), currentServer);
if (configManager.getOverlayServers().contains(currentServer)) {
waitingForReady.add(player);
logger.debug("[CrossTeleportMod] Added {} to waitingForReady set", player.getUsername());
} else {
OverlayManager.hideOverlay(player);
logger.debug("[CrossTeleportMod] Hiding overlay for {}", player.getUsername());
}
}
/**
* 判断玩家是否已经在目标服务器
* @param player 玩家对象
* @param serverName 目标服务器名
* @return 是否已在该服务器
*/
private boolean isAlreadyOnServer(Player player, String serverName) {
return player.getCurrentServer()
.map(s -> s.getServerInfo().getName().equalsIgnoreCase(serverName))
.orElse(false);
}
}

View File

@ -1,6 +1,6 @@
package com.leisuretimedock.crossplugin.manager;
import com.leisuretimedock.crossplugin.handler.PluginChannelHandler;
import com.leisuretimedock.crossplugin.listener.PluginMessageListener;
import com.leisuretimedock.crossplugin.messages.I18n;
import com.leisuretimedock.crossplugin.messages.I18nKeyEnum;
import com.velocitypowered.api.proxy.Player;
@ -29,7 +29,7 @@ public class OverlayManager {
data.flush();
player.sendPluginMessage(
PluginChannelHandler.CHANNEL_ID,
PluginMessageListener.CHANNEL_ID,
out.toByteArray()
);
} catch (Exception e) {
@ -56,7 +56,7 @@ public class OverlayManager {
}
player.sendPluginMessage(
PluginChannelHandler.CHANNEL_ID,
PluginMessageListener.CHANNEL_ID,
out.toByteArray()
);

View File

@ -1,6 +1,6 @@
ltd.plugin.trans.no_permission=你没有权限传送到该服务器!({0})
ltd.plugin.trans.server_not_found=目标服务器不存在!({0})
ltd.plugin.trans.already_on_server=你已经在该服务器上了。
ltd.plugin.trans.failed.no_permission=你没有权限传送到该服务器!({0})
ltd.plugin.trans.failed.server_not_found=目标服务器不存在!({0})
ltd.plugin.trans.failed.already_on_server=你已经在该服务器上了。
ltd.plugin.send_server_list.failed=发送服务器列表失败。
ltd.plugin.command.no_permission=你没有权限重载去执行该指令,需要权限节点:{0}
ltd.plugin.reload.successful=配置已重新加载。