From 9fbf1773ec05dc6066363b91934a8b16c90be31f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:35:08 +0000 Subject: [PATCH] Fix ClickEvent API, redesign link messages with inline click-to-copy/suggest, fix help visibility for MC players, fix OP sync on reload Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com> --- .../com/xujiayao/discord_mc_chat/DMCC.java | 8 ++ core/src/main/resources/lang/en_us.yml | 21 ++++- core/src/main/resources/lang/zh_cn.yml | 21 ++++- .../minecraft/commands/MinecraftCommands.java | 20 +++- .../events/MinecraftEventHandler.java | 94 +++++++++++++++---- 5 files changed, 135 insertions(+), 29 deletions(-) diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/DMCC.java b/core/src/main/java/com/xujiayao/discord_mc_chat/DMCC.java index 7ca9e5fb..412c3a60 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/DMCC.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/DMCC.java @@ -4,6 +4,7 @@ import com.xujiayao.discord_mc_chat.client.ClientDMCC; import com.xujiayao.discord_mc_chat.commands.CommandManager; import com.xujiayao.discord_mc_chat.network.NetworkManager; import com.xujiayao.discord_mc_chat.server.ServerDMCC; +import com.xujiayao.discord_mc_chat.server.linking.OpSyncManager; import com.xujiayao.discord_mc_chat.utils.CryptUtils; import com.xujiayao.discord_mc_chat.utils.config.ConfigManager; import com.xujiayao.discord_mc_chat.utils.config.ModeManager; @@ -147,6 +148,13 @@ public class DMCC { } LOGGER.info(I18nManager.getDmccTranslation("main.init.success")); + + // Trigger OP sync after successful initialization + // In single_server mode on first start, this is redundant with the ServerStarted event sync, + // but on reload it is necessary since ServerStarted won't fire again. + // In standalone mode, this syncs all currently connected clients. + OpSyncManager.syncAll(); + return true; }).get(); } catch (Exception e) { diff --git a/core/src/main/resources/lang/en_us.yml b/core/src/main/resources/lang/en_us.yml index f9bd5411..a3130847 100644 --- a/core/src/main/resources/lang/en_us.yml +++ b/core/src/main/resources/lang/en_us.yml @@ -131,8 +131,15 @@ commands: player_only: "This command can only be used by a Minecraft player." discord_only: "This command can only be used from Discord." not_available: "This command is not available in the current mode." - already_linked: "Your Minecraft account is already linked to Discord user \"{}\"." - code_generated: "Your verification code is: {0}. Use /link {0} on Discord to complete linking. The code expires in 5 minutes." + already_linked: "Your Minecraft account is already linked to Discord user \"{}\". Run " + already_linked_unlink: "/dmcc unlink" + already_linked_suffix: " to unlink." + code_generated_prefix: "Your verification code is: " + code_generated_suffix: ". Use " + code_generated_discord_cmd: "/link code: {}" + code_generated_tail: " on Discord to complete linking. The code expires in 5 minutes. If expired, run " + code_generated_refresh_cmd: "/dmcc link" + code_generated_refresh_tail: " in-game to get a new code." invalid_code: "Invalid or expired verification code." success: "Successfully linked to Minecraft player \"{}\"!" uuid_already_linked: "This Minecraft account is already linked to another Discord user." @@ -271,9 +278,13 @@ linking: refreshed: "Refreshed verification code {} for player {}!" consumed: "Verification code consumed by player {}: {}!" player_join: - not_linked: "Your Minecraft account is not linked to Discord. Use /dmcc link to get a verification code." - code_hint: "Use /link {} on Discord within 5 minutes to link your account." - click_to_copy: "Click to copy verification code" + not_linked_prefix: "Your Minecraft account is not linked to Discord. Use " + not_linked_discord_cmd: "/link code: {}" + not_linked_middle: " on Discord to complete linking. The code expires in 5 minutes. If expired, run " + not_linked_refresh_cmd: "/dmcc link" + not_linked_refresh_tail: " in-game to get a new code." + click_to_copy: "Click to copy" + click_to_run: "Click to fill command" minecraft: translations: diff --git a/core/src/main/resources/lang/zh_cn.yml b/core/src/main/resources/lang/zh_cn.yml index 55efc7ab..1e4a14e2 100644 --- a/core/src/main/resources/lang/zh_cn.yml +++ b/core/src/main/resources/lang/zh_cn.yml @@ -131,8 +131,15 @@ commands: player_only: "此命令只能由 Minecraft 玩家使用。" discord_only: "此命令只能在 Discord 上使用。" not_available: "此命令在当前模式下不可用。" - already_linked: "你的 Minecraft 账户已绑定到 Discord 用户 \"{}\"。" - code_generated: "你的验证码是:{0}。在 Discord 上使用 /link {0} 来完成绑定。验证码将在 5 分钟后过期。" + already_linked: "你的 Minecraft 账户已绑定到 Discord 用户 \"{}\"。执行 " + already_linked_unlink: "/dmcc unlink" + already_linked_suffix: " 解除绑定。" + code_generated_prefix: "你的验证码是:" + code_generated_suffix: "。在 Discord 上使用 " + code_generated_discord_cmd: "/link code: {}" + code_generated_tail: " 命令来完成绑定。验证码将在 5 分钟后过期。若过期可在游戏内执行 " + code_generated_refresh_cmd: "/dmcc link" + code_generated_refresh_tail: " 获取新验证码。" invalid_code: "无效或已过期的验证码。" success: "成功绑定 Minecraft 玩家 \"{}\"!" uuid_already_linked: "此 Minecraft 账户已绑定到另一个 Discord 用户。" @@ -271,9 +278,13 @@ linking: refreshed: "已为玩家 {} 刷新验证码 {}!" consumed: "验证码已被玩家 {} 使用:{}!" player_join: - not_linked: "你的 Minecraft 账户尚未绑定 Discord。使用 /dmcc link 获取验证码。" - code_hint: "在 5 分钟内在 Discord 上使用 /link {} 来绑定你的账户。" - click_to_copy: "点击复制验证码" + not_linked_prefix: "你的 Minecraft 账户尚未与 Discord 用户绑定。在 Discord 上使用 " + not_linked_discord_cmd: "/link code: {}" + not_linked_middle: " 命令来完成绑定。验证码将在 5 分钟后过期。若过期可在游戏内执行 " + not_linked_refresh_cmd: "/dmcc link" + not_linked_refresh_tail: " 获取新验证码。" + click_to_copy: "点击复制" + click_to_run: "点击填入命令" minecraft: translations: diff --git a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java index 037f1996..14708ed2 100644 --- a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java +++ b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/commands/MinecraftCommands.java @@ -36,13 +36,13 @@ public class MinecraftCommands { var root = literal("dmcc") .requires(source -> source.hasPermission(ConfigManager.getInt("command_permission_levels.help", -1))) .executes(ctx -> { - CommandManager.execute(new MinecraftCommandSender(ctx.getSource()), "help"); + CommandManager.execute(createSenderForSource(ctx.getSource()), "help"); return 1; }); var help = literal("help") .requires(source -> source.hasPermission(ConfigManager.getInt("command_permission_levels.help", -1))) .executes(ctx -> { - CommandManager.execute(new MinecraftCommandSender(ctx.getSource()), "help"); + CommandManager.execute(createSenderForSource(ctx.getSource()), "help"); return 1; }); var info = literal("info") @@ -103,6 +103,22 @@ public class MinecraftCommands { .then(unlink)); } + /** + * Creates the appropriate command sender based on whether the source is a player or console. + *

+ * Players get a {@link MinecraftPlayerCommandSender} which provides player context + * for commands like link/unlink. Console/command blocks get a plain {@link MinecraftCommandSender}. + * + * @param source The command source stack. + * @return The appropriate command sender. + */ + private static LocalCommandSender createSenderForSource(CommandSourceStack source) { + if (source.getEntity() instanceof ServerPlayer) { + return new MinecraftPlayerCommandSender(source); + } + return new MinecraftCommandSender(source); + } + /** * Command sender implementation for Minecraft command sources. *

diff --git a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java index bf7f94ab..2d50fe22 100644 --- a/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java +++ b/minecraft/src/main/java/com/xujiayao/discord_mc_chat/minecraft/events/MinecraftEventHandler.java @@ -162,13 +162,9 @@ public class MinecraftEventHandler { // Direct access to server-side managers if (!LinkedAccountManager.isMinecraftUuidLinked(playerUuid)) { String code = VerificationCodeManager.generateOrRefreshCode(playerUuid, playerName); - // Notify the player in-game + // Notify the player in-game with inline clickable elements ServerPlayer sp = event.serverPlayer(); - sp.sendSystemMessage(Component.literal( - I18nManager.getDmccTranslation("linking.player_join.not_linked"))); - sp.sendSystemMessage(Component.literal( - I18nManager.getDmccTranslation("linking.player_join.code_hint", code))); - sp.sendSystemMessage(buildClickableCode(code)); + sp.sendSystemMessage(buildNotLinkedMessage(code)); } } case "multi_server_client" -> { @@ -372,12 +368,9 @@ public class MinecraftEventHandler { ServerPlayer player = serverInstance.getPlayerList().getPlayer(uuid); if (player != null) { if (event.alreadyLinked()) { - player.sendSystemMessage(Component.literal( - I18nManager.getDmccTranslation("commands.link.already_linked", event.discordName()))); + player.sendSystemMessage(buildAlreadyLinkedMessage(event.discordName())); } else if (event.code() != null) { - player.sendSystemMessage(Component.literal( - I18nManager.getDmccTranslation("commands.link.code_generated", event.code()))); - player.sendSystemMessage(buildClickableCode(event.code())); + player.sendSystemMessage(buildCodeGeneratedMessage(event.code())); } } } catch (Exception ignored) { @@ -542,21 +535,88 @@ public class MinecraftEventHandler { } /** - * Builds a clickable Component for a verification code. - * When clicked, the code is copied to the player's clipboard. + * Builds a rich Component for the "not linked" player join notification. + * Contains inline clickable elements: [/link code: CODE] for copy-to-clipboard + * and [/dmcc link] for suggest-command. * * @param code The verification code. + * @return A rich Component with inline clickable elements. + */ + private static Component buildNotLinkedMessage(String code) { + return Component.empty() + .append(Component.literal(I18nManager.getDmccTranslation("linking.player_join.not_linked_prefix"))) + .append(buildCopyToClipboard(I18nManager.getDmccTranslation("linking.player_join.not_linked_discord_cmd", code))) + .append(Component.literal(I18nManager.getDmccTranslation("linking.player_join.not_linked_middle"))) + .append(buildSuggestCommand("/dmcc link", I18nManager.getDmccTranslation("linking.player_join.not_linked_refresh_cmd"))) + .append(Component.literal(I18nManager.getDmccTranslation("linking.player_join.not_linked_refresh_tail"))); + } + + /** + * Builds a rich Component for the "code generated" response from /dmcc link. + * Contains inline clickable elements: [/link code: CODE] for copy-to-clipboard + * and [/dmcc link] for suggest-command. + * + * @param code The verification code. + * @return A rich Component with inline clickable elements. + */ + private static Component buildCodeGeneratedMessage(String code) { + return Component.empty() + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.code_generated_prefix"))) + .append(buildCopyToClipboard(code)) + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.code_generated_suffix"))) + .append(buildCopyToClipboard(I18nManager.getDmccTranslation("commands.link.code_generated_discord_cmd", code))) + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.code_generated_tail"))) + .append(buildSuggestCommand("/dmcc link", I18nManager.getDmccTranslation("commands.link.code_generated_refresh_cmd"))) + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.code_generated_refresh_tail"))); + } + + /** + * Builds a rich Component for the "already linked" response from /dmcc link. + * Contains an inline clickable [/dmcc unlink] suggest-command element. + * + * @param discordName The Discord user's display name. + * @return A rich Component with inline clickable element. + */ + private static Component buildAlreadyLinkedMessage(String discordName) { + return Component.empty() + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.already_linked", discordName))) + .append(buildSuggestCommand("/dmcc unlink", I18nManager.getDmccTranslation("commands.link.already_linked_unlink"))) + .append(Component.literal(I18nManager.getDmccTranslation("commands.link.already_linked_suffix"))); + } + + /** + * Builds a clickable Component that copies the given text to clipboard when clicked. + * Displayed in green bold with brackets. + * + * @param text The text to copy and display. * @return A clickable Component. */ - private static Component buildClickableCode(String code) { - return Component.literal("[" + code + "]").withStyle(style -> style - .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, code)) - .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + private static Component buildCopyToClipboard(String text) { + return Component.literal("[" + text + "]").withStyle(style -> style + .withClickEvent(new ClickEvent.CopyToClipboard(text)) + .withHoverEvent(new HoverEvent.ShowText( Component.literal(I18nManager.getDmccTranslation("linking.player_join.click_to_copy")))) .withColor(ChatFormatting.GREEN) .withBold(true)); } + /** + * Builds a clickable Component that suggests a command (fills the chat input) when clicked. + * Displayed in green bold with brackets. + * + * @param command The command to suggest (fill into chat input). + * @param displayText The text to display in the bracket. + * @return A clickable Component. + */ + private static Component buildSuggestCommand(String command, String displayText) { + return Component.literal("[" + displayText + "]").withStyle(style -> style + .withClickEvent(new ClickEvent.SuggestCommand(command)) + .withHoverEvent(new HoverEvent.ShowText( + Component.literal(I18nManager.getDmccTranslation("linking.player_join.click_to_run")))) + .withColor(ChatFormatting.GREEN) + .withBold(true)); + } + /** * Builds an InfoResponsePacket containing real-time metrics of the Minecraft server. *