mirror of
https://github.com/System-End/Discord-MC-Chat.git
synced 2026-04-19 18:35:15 +00:00
Fix compilation errors, NPE, help visibility, auto-complete, click-to-copy, file path rename
Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com>
This commit is contained in:
parent
04874111ed
commit
1945cf2e19
9 changed files with 93 additions and 28 deletions
|
|
@ -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 全量同步时被覆盖,这是预期行为。
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,19 @@ public interface Command {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this command should appear in auto-complete suggestions for the execute command.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
|
|
|
|||
|
|
@ -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<String> suggestions) {
|
||||
if (!cmd.isAutoCompletable()) {
|
||||
return;
|
||||
}
|
||||
int requiredOp = ConfigManager.getInt("command_permission_levels." + cmd.name(), 4);
|
||||
if (opLevel >= requiredOp) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -22,13 +22,13 @@ import static com.xujiayao.discord_mc_chat.Constants.LOGGER;
|
|||
* <p>
|
||||
* 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<String, List<LinkEntry>> LINKED_ACCOUNTS = new ConcurrentHashMap<>();
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ public class LinkedAccountManager {
|
|||
* Clears all linked accounts from memory without writing to disk.
|
||||
* <p>
|
||||
* 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;
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -273,6 +273,7 @@ linking:
|
|||
player_join:
|
||||
not_linked: "你的 Minecraft 账户尚未绑定 Discord。使用 /dmcc link 获取验证码。"
|
||||
code_hint: "在 5 分钟内在 Discord 上使用 /link {} 来绑定你的账户。"
|
||||
click_to_copy: "点击复制验证码"
|
||||
|
||||
minecraft:
|
||||
translations:
|
||||
|
|
|
|||
|
|
@ -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<net.minecraft.server.players.ServerOpListEntry> 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<ServerOpListEntry> 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<NameAndId> 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.
|
||||
*
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue