From 1945cf2e194d232c33781db08dd531c6f3dcbf8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:42:26 +0000 Subject: [PATCH] Fix compilation errors, NPE, help visibility, auto-complete, click-to-copy, file path rename Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com> --- README.md | 8 +-- .../discord_mc_chat/commands/Command.java | 13 +++++ .../commands/CommandAutoCompleter.java | 4 ++ .../commands/impl/LinkCommand.java | 17 ++++-- .../commands/impl/UnlinkCommand.java | 14 ++++- .../server/linking/LinkedAccountManager.java | 10 ++-- core/src/main/resources/lang/en_us.yml | 1 + core/src/main/resources/lang/zh_cn.yml | 1 + .../events/MinecraftEventHandler.java | 53 +++++++++++++------ 9 files changed, 93 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5d321442..76221dec 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ DMCC 所有运行模式都基于一个统一的通信模型,该模型包含两 - **一个 Discord 账户可关联多个 Minecraft 账户**(方便玩家管理大号与小号)。 - **一个 Minecraft 账户只能关联一个 Discord 账户**(确保游戏内身份的绝对唯一性)。 -- **数据持久化**: 绑定关系作为永久数据存储在 `Server` 端的 `account_linking/linked_accounts.json` 中,以 Discord ID 为主键。 -- **存储约束**: `linked_accounts.json` 中仅存储绑定关系最小必要字段(Discord ID、Minecraft UUID、添加时间),**不得额外存储** +- **数据持久化**: 绑定关系作为永久数据存储在 `Server` 端的 `account_linking/links.json` 中,以 Discord ID 为主键。 +- **存储约束**: `links.json` 中仅存储绑定关系最小必要字段(Discord ID、Minecraft UUID、添加时间),**不得额外存储** Discord 用户名或 Minecraft 玩家名;显示名称在查询时实时解析。 ### 4.2 安全绑定工作流 (严格的 MC 优先原则) @@ -75,7 +75,7 @@ DMCC 所有运行模式都基于一个统一的通信模型,该模型包含两 - 若该玩家存在未过期验证码,返回同一验证码并将过期时间重置为“当前时间 + 5 分钟”; - 若验证码已过期或不存在,生成并返回新验证码(同样 5 分钟有效)。 4. **确认所有权**: 玩家前往 Discord,使用斜杠命令 `/link A7X9P2`。 -5. **完成绑定**: Server 验证代码有效后,将该 Discord ID 与 MC UUID 写入 `linked_accounts.json`,并使该验证码失效。 +5. **完成绑定**: Server 验证代码有效后,将该 Discord ID 与 MC UUID 写入 `links.json`,并使该验证码失效。 ### 4.3 解绑与查询工作流 @@ -138,7 +138,7 @@ Discord 用户的身份将通过以下规则在 Server 端结算为一个具体 1. **全量同步原则**: 每次同步均执行“全量重算 + 全量应用”,而非增量补丁。 2. **强制覆盖原则**: DMCC 将重置 Minecraft 服务器当前 OP 列表,并依据 DMCC 配置中的映射规则重新分配。 -3. **绑定缺失回退**: 若某玩家解除绑定后在 `linked_accounts.json` 中不再出现,则在下一次全量同步中该玩家 OP 等级将被重置为 0。 +3. **绑定缺失回退**: 若某玩家解除绑定后在 `links.json` 中不再出现,则在下一次全量同步中该玩家 OP 等级将被重置为 0。 4. **关闭时不干预**: 若 `sync_op_level_to_minecraft=false`,解绑不触发 OP 回收,服务器维持原样。 5. **与原生 `/op` 的关系**: 开启后,管理员在游戏内使用原生命令 `/op` 手动授予的结果会在下一次 DMCC 全量同步时被覆盖,这是预期行为。 diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/Command.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/Command.java index 12b22d79..ac074c8b 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/Command.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/Command.java @@ -53,6 +53,19 @@ public interface Command { return false; } + /** + * Whether this command should appear in auto-complete suggestions for the execute command. + *

+ * Commands that are only meaningful for specific sender types (e.g., link/unlink + * which require player or Discord context) should return false. + * This does NOT affect whether the command can be executed — only its auto-complete visibility. + * + * @return true if this command should appear in auto-complete, false otherwise. + */ + default boolean isAutoCompletable() { + return true; + } + /** * Whether this command should be visible in help listings for the given sender. *

diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java index f046519e..3eabfe0f 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/CommandAutoCompleter.java @@ -68,6 +68,7 @@ public class CommandAutoCompleter { CommandManager.getCommands().stream() .filter(cmd -> cmd.name().startsWith(commandName)) + .filter(Command::isAutoCompletable) .sorted(Comparator.comparing(Command::name)) .forEach(cmd -> addCommandIfAuthorized(cmd, opLevel, suggestions)); @@ -308,6 +309,9 @@ public class CommandAutoCompleter { * @param suggestions The list to append to. */ private static void addCommandIfAuthorized(Command cmd, int opLevel, List suggestions) { + if (!cmd.isAutoCompletable()) { + return; + } int requiredOp = ConfigManager.getInt("command_permission_levels." + cmd.name(), 4); if (opLevel >= requiredOp) { StringBuilder builder = new StringBuilder(); diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinkCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinkCommand.java index cdec02e3..21ea9cc3 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinkCommand.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinkCommand.java @@ -7,6 +7,8 @@ import com.xujiayao.discord_mc_chat.network.packets.commands.link.LinkRequestPac import com.xujiayao.discord_mc_chat.server.linking.LinkedAccountManager; import com.xujiayao.discord_mc_chat.server.linking.VerificationCodeManager; import com.xujiayao.discord_mc_chat.utils.config.ModeManager; +import com.xujiayao.discord_mc_chat.utils.events.CoreEvents; +import com.xujiayao.discord_mc_chat.utils.events.EventManager; import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; /** @@ -106,10 +108,15 @@ public class LinkCommand implements Command { return I18nManager.getDmccTranslation("commands.link.description"); } + @Override + public boolean isAutoCompletable() { + return false; + } + @Override public boolean isVisibleInHelp(CommandSender sender) { - // Always visible - link is available to players and Discord users - return true; + // Only show link in help for Minecraft players or Discord users + return (sender instanceof PlayerContextProvider) || (sender instanceof DiscordUserContextProvider); } @Override @@ -148,11 +155,13 @@ public class LinkCommand implements Command { if (LinkedAccountManager.isMinecraftUuidLinked(uuid)) { String discordId = LinkedAccountManager.getDiscordIdByMinecraftUuid(uuid); String discordName = LinkedAccountManager.resolveDiscordName(discordId); - sender.reply(I18nManager.getDmccTranslation("commands.link.already_linked", discordName)); + // Post event so Minecraft module can display with rich formatting + EventManager.post(new CoreEvents.LinkCodeResponseEvent(uuid, null, true, discordName)); return; } String code = VerificationCodeManager.generateOrRefreshCode(uuid, name); - sender.reply(I18nManager.getDmccTranslation("commands.link.code_generated", code)); + // Post event so Minecraft module can display with click-to-copy + EventManager.post(new CoreEvents.LinkCodeResponseEvent(uuid, code, false, "")); } case "multi_server_client" -> { // Send request to standalone server via network diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/UnlinkCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/UnlinkCommand.java index 4dd03309..f9f444ce 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/UnlinkCommand.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/UnlinkCommand.java @@ -33,10 +33,15 @@ public class UnlinkCommand implements Command { return I18nManager.getDmccTranslation("commands.unlink.description"); } + @Override + public boolean isAutoCompletable() { + return false; + } + @Override public boolean isVisibleInHelp(CommandSender sender) { - // Always visible - unlink is available to players and Discord users - return true; + // Only show unlink in help for Minecraft players or Discord users + return (sender instanceof LinkCommand.PlayerContextProvider) || (sender instanceof LinkCommand.DiscordUserContextProvider); } @Override @@ -57,6 +62,11 @@ public class UnlinkCommand implements Command { String uuid = player.getPlayerUuid(); String name = player.getPlayerName(); + if (uuid == null) { + sender.reply(I18nManager.getDmccTranslation("commands.unlink.not_available")); + return; + } + switch (ModeManager.getMode()) { case "single_server" -> { // Look up the linked Discord ID before unlinking so we can show it diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/server/linking/LinkedAccountManager.java b/core/src/main/java/com/xujiayao/discord_mc_chat/server/linking/LinkedAccountManager.java index 4bfa4b95..ff5f88b3 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/server/linking/LinkedAccountManager.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/server/linking/LinkedAccountManager.java @@ -22,13 +22,13 @@ import static com.xujiayao.discord_mc_chat.Constants.LOGGER; *

* One Discord account can link multiple Minecraft accounts (1:N). * One Minecraft account can only link to one Discord account (N:1 uniqueness). - * Data is stored in {@code account_linking/linked_accounts.json} in the DMCC config directory. + * Data is stored in {@code account_linking/links.json} in the DMCC config directory. * * @author Xujiayao */ public class LinkedAccountManager { - private static final Path LINKS_FILE = Paths.get("./config/discord_mc_chat/account_linking/linked_accounts.json"); + private static final Path LINKS_FILE = Paths.get("./config/discord_mc_chat/account_linking/links.json"); private static final ConcurrentHashMap> LINKED_ACCOUNTS = new ConcurrentHashMap<>(); @@ -127,7 +127,7 @@ public class LinkedAccountManager { * Clears all linked accounts from memory without writing to disk. *

* This intentionally does NOT save to disk, so users can manually edit - * {@code linked_accounts.json} while DMCC is running and reload to apply their changes. + * {@code links.json} while DMCC is running and reload to apply their changes. * Any in-memory changes that were not yet persisted via {@link #save()} will be lost. * In practice, all mutations (link/unlink) call {@link #save()} immediately, * so no data is lost under normal operation. @@ -195,6 +195,10 @@ public class LinkedAccountManager { * @return The Discord user ID that was unlinked from, or null if the UUID was not linked. */ public static synchronized String unlinkByMinecraftUuid(String minecraftUuid, String minecraftName) { + if (minecraftUuid == null) { + return null; + } + String discordId = UUID_TO_DISCORD.remove(minecraftUuid); if (discordId == null) { return null; diff --git a/core/src/main/resources/lang/en_us.yml b/core/src/main/resources/lang/en_us.yml index d7764747..f9bd5411 100644 --- a/core/src/main/resources/lang/en_us.yml +++ b/core/src/main/resources/lang/en_us.yml @@ -273,6 +273,7 @@ linking: 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" minecraft: translations: diff --git a/core/src/main/resources/lang/zh_cn.yml b/core/src/main/resources/lang/zh_cn.yml index 07dd35d3..55efc7ab 100644 --- a/core/src/main/resources/lang/zh_cn.yml +++ b/core/src/main/resources/lang/zh_cn.yml @@ -273,6 +273,7 @@ linking: player_join: not_linked: "你的 Minecraft 账户尚未绑定 Discord。使用 /dmcc link 获取验证码。" code_hint: "在 5 分钟内在 Discord 上使用 /link {} 来绑定你的账户。" + click_to_copy: "点击复制验证码" minecraft: translations: 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 4785d102..bf7f94ab 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 @@ -23,16 +23,22 @@ import com.xujiayao.discord_mc_chat.utils.events.CoreEvents; import com.xujiayao.discord_mc_chat.utils.events.EventManager; import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; import net.minecraft.advancements.DisplayInfo; +import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSource; import net.minecraft.commands.CommandSourceStack; import net.minecraft.core.Holder; import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.MinecraftServer; import net.minecraft.server.ServerTickRateManager; import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.players.NameAndId; +import net.minecraft.server.players.PlayerList; +import net.minecraft.server.players.ServerOpList; +import net.minecraft.server.players.ServerOpListEntry; import net.minecraft.stats.StatType; import net.minecraft.util.TimeUtil; import net.minecraft.world.level.GameRules; @@ -162,6 +168,7 @@ public class MinecraftEventHandler { I18nManager.getDmccTranslation("linking.player_join.not_linked"))); sp.sendSystemMessage(Component.literal( I18nManager.getDmccTranslation("linking.player_join.code_hint", code))); + sp.sendSystemMessage(buildClickableCode(code)); } } case "multi_server_client" -> { @@ -369,7 +376,8 @@ public class MinecraftEventHandler { I18nManager.getDmccTranslation("commands.link.already_linked", event.discordName()))); } else if (event.code() != null) { player.sendSystemMessage(Component.literal( - I18nManager.getDmccTranslation("linking.player_join.code_hint", event.code()))); + I18nManager.getDmccTranslation("commands.link.code_generated", event.code()))); + player.sendSystemMessage(buildClickableCode(event.code())); } } } catch (Exception ignored) { @@ -405,17 +413,16 @@ public class MinecraftEventHandler { serverInstance.execute(() -> { try { - net.minecraft.server.players.PlayerList playerList = serverInstance.getPlayerList(); - net.minecraft.server.players.ServerOpList opList = playerList.getOps(); + PlayerList playerList = serverInstance.getPlayerList(); + ServerOpList opList = playerList.getOps(); // Step 1: De-op all currently opped players // Copy the list to avoid ConcurrentModificationException - List currentOps = - new ArrayList<>(opList.getEntries()); - for (net.minecraft.server.players.ServerOpListEntry op : currentOps) { - com.mojang.authlib.GameProfile profile = op.getUser(); - if (profile != null) { - playerList.deop(profile); + List currentOps = new ArrayList<>(opList.getEntries()); + for (ServerOpListEntry op : currentOps) { + NameAndId nameAndId = op.getUser(); + if (nameAndId != null) { + playerList.deop(nameAndId); } } @@ -427,15 +434,15 @@ public class MinecraftEventHandler { try { UUID uuid = UUID.fromString(uuidStr); - com.mojang.authlib.GameProfile profile = serverInstance.getProfileCache() - .get(uuid).orElse(null); - if (profile == null) { - // Profile not in cache; skip this entry as we can't op without a valid profile + Optional nameAndIdOpt = serverInstance.services().nameToIdCache().get(uuid); + if (nameAndIdOpt.isEmpty()) { + // Profile not in cache; skip this entry continue; } + NameAndId nameAndId = nameAndIdOpt.get(); // Add the OP entry with the exact desired level - opList.add(new net.minecraft.server.players.ServerOpListEntry( - profile, opLevel, opList.canBypassPlayerLimit(profile))); + opList.add(new ServerOpListEntry( + nameAndId, opLevel, opList.canBypassPlayerLimit(nameAndId))); } catch (Exception ignored) { } } @@ -534,6 +541,22 @@ public class MinecraftEventHandler { } } + /** + * Builds a clickable Component for a verification code. + * When clicked, the code is copied to the player's clipboard. + * + * @param code The verification code. + * @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, + Component.literal(I18nManager.getDmccTranslation("linking.player_join.click_to_copy")))) + .withColor(ChatFormatting.GREEN) + .withBold(true)); + } + /** * Builds an InfoResponsePacket containing real-time metrics of the Minecraft server. *