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>
This commit is contained in:
copilot-swe-agent[bot] 2026-03-10 14:35:08 +00:00 committed by Jason Xu
parent 1945cf2e19
commit 9fbf1773ec
5 changed files with 135 additions and 29 deletions

View file

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

View file

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

View file

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

View file

@ -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.
* <p>
* 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.
* <p>

View file

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