From 7afea647a508daf84378b44deb07d0beabb4e67f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 04:30:32 +0000 Subject: [PATCH] feat: persist offline player name in LinkEntry for proper display in links command Add offlinePlayerName field to LinkEntry record, stored only for offline-mode UUIDs (version 3) since Mojang API cannot resolve them. Online-mode players continue to use real-time Mojang API resolution. The field is omitted from JSON when null for clean serialization. MojangUtils.resolvePlayerName() now accepts an optional fallback name for offline UUIDs, returning "N/A" when no name is available. Updated README.md storage constraints and query display rules to reflect the new offline player name persistence. Co-authored-by: Xujiayao <58985541+Xujiayao@users.noreply.github.com> --- README.md | 8 +++-- .../commands/impl/LinksCommand.java | 2 +- .../server/linking/LinkedAccountManager.java | 30 ++++++++++++++++--- .../discord_mc_chat/utils/MojangUtils.java | 22 ++++++++++++-- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1e6da71c..69903a3c 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,9 @@ DMCC 所有运行模式都基于一个统一的通信模型,该模型包含两 - **一个 Discord 账户可关联多个 Minecraft 账户**(方便玩家管理大号与小号)。 - **一个 Minecraft 账户只能关联一个 Discord 账户**(确保游戏内身份的绝对唯一性)。 - **数据持久化**: 绑定关系作为永久数据存储在 `Server` 端的 `account_linking/links.json` 中,以 Discord ID 为主键。 -- **存储约束**: `links.json` 中仅存储绑定关系最小必要字段(Discord ID、Minecraft UUID、添加时间),**不得额外存储** - Discord 用户名或 Minecraft 玩家名;显示名称在查询时实时解析。 +- **存储约束**: `links.json` 中存储绑定关系最小必要字段(Discord ID、Minecraft UUID、添加时间)。对于离线模式玩家, + 额外存储 `offlinePlayerName`(绑定时的玩家名),因为离线 UUID 无法通过 Mojang API 反查玩家名。 + 在线模式玩家不存储玩家名,显示名称在查询时通过 Mojang API 实时解析。Discord 用户名始终不存储,查询时实时解析。 ### 4.2 安全绑定工作流 (严格的 MC 优先原则) @@ -82,7 +83,8 @@ DMCC 所有运行模式都基于一个统一的通信模型,该模型包含两 - **Discord `/unlink`**: 直接取消该 Discord 用户名下的所有 Minecraft 绑定(无需二次确认)。 - **Minecraft `/dmcc unlink`**: 直接取消“当前执行玩家”对应的绑定关系(无需二次确认)。 -- **查询展示规则**: `links` 查询时实时解析显示名称,若无法解析则回退显示 UUID / Discord ID。 +- **查询展示规则**: `links` 查询时实时解析显示名称;在线模式玩家通过 Mojang API 解析,离线模式玩家使用绑定时记录的玩家名。 + 若无法解析则显示 "N/A"(Minecraft 玩家名)或 Discord ID。 ### 4.4 同步与跨平台交互 diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinksCommand.java b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinksCommand.java index 5562be52..4343de31 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinksCommand.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/commands/impl/LinksCommand.java @@ -71,7 +71,7 @@ public class LinksCommand implements Command { for (LinkedAccountManager.LinkEntry link : links) { String time = DATE_FORMATTER.format(Instant.ofEpochMilli(link.linkedAt())); - String mcName = MojangUtils.resolvePlayerName(link.minecraftUuid()); + String mcName = MojangUtils.resolvePlayerName(link.minecraftUuid(), link.offlinePlayerName()); builder.append("\n - MC: ").append(mcName).append(" (").append(link.minecraftUuid()).append(")"); builder.append(" (").append(time).append(")"); } 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 b680ada5..0efe8124 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 @@ -1,5 +1,6 @@ package com.xujiayao.discord_mc_chat.server.linking; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.type.TypeReference; import com.xujiayao.discord_mc_chat.utils.i18n.I18nManager; @@ -11,6 +12,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -151,7 +153,7 @@ public class LinkedAccountManager { } LINKED_ACCOUNTS.computeIfAbsent(discordId, k -> new ArrayList<>()) - .add(new LinkEntry(minecraftUuid, System.currentTimeMillis())); + .add(new LinkEntry(minecraftUuid, System.currentTimeMillis(), isOfflineUuid(minecraftUuid) ? minecraftName : null)); UUID_TO_DISCORD.put(minecraftUuid, discordId); LOGGER.info(I18nManager.getDmccTranslation("linking.manager.linked", discordName, discordId, minecraftName, minecraftUuid)); @@ -260,12 +262,32 @@ public class LinkedAccountManager { return Collections.unmodifiableMap(LINKED_ACCOUNTS); } + /** + * Checks if a Minecraft UUID is an offline-mode UUID (version 3). + * + * @param uuidString The UUID string. + * @return true if the UUID is offline-mode (version 3), false otherwise. + */ + private static boolean isOfflineUuid(String uuidString) { + try { + return UUID.fromString(uuidString).version() == 3; + } catch (Exception e) { + return false; + } + } + /** * A linked Minecraft account entry. * - * @param minecraftUuid The UUID of the linked Minecraft account. - * @param linkedAt The timestamp (epoch millis) when the link was created. + * @param minecraftUuid The UUID of the linked Minecraft account. + * @param linkedAt The timestamp (epoch millis) when the link was created. + * @param offlinePlayerName The player name stored at link time for offline-mode UUIDs only. + * {@code null} for online-mode players (resolvable via Mojang API). */ - public record LinkEntry(String minecraftUuid, long linkedAt) { + public record LinkEntry( + String minecraftUuid, + long linkedAt, + @JsonInclude(JsonInclude.Include.NON_NULL) String offlinePlayerName + ) { } } diff --git a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/MojangUtils.java b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/MojangUtils.java index 60f50777..2bbcd1b9 100644 --- a/core/src/main/java/com/xujiayao/discord_mc_chat/utils/MojangUtils.java +++ b/core/src/main/java/com/xujiayao/discord_mc_chat/utils/MojangUtils.java @@ -33,6 +33,22 @@ public class MojangUtils { * @return The resolved player name, or the original UUID string if resolution fails. */ public static String resolvePlayerName(String uuidString) { + return resolvePlayerName(uuidString, null); + } + + /** + * Resolves a Minecraft player name from a UUID string, with an optional fallback name + * for offline-mode UUIDs. + *

+ * If the UUID is an offline-mode UUID (version 3), {@code offlineFallbackName} is returned + * if non-null; otherwise "N/A" is returned. For online-mode UUIDs (version 4), the Mojang + * session server is queried. Network failures fall back to the raw UUID. + * + * @param uuidString The UUID string (standard dashed format). + * @param offlineFallbackName The player name to use for offline UUIDs, or null. + * @return The resolved player name, or the fallback/"N/A"/UUID string if resolution fails. + */ + public static String resolvePlayerName(String uuidString, String offlineFallbackName) { String cached = NAME_CACHE.get(uuidString); if (cached != null) { return cached; @@ -44,8 +60,10 @@ public class MojangUtils { // Check if this is an offline-mode UUID (version 3) if (uuid.version() == 3) { // Offline UUIDs are generated from "OfflinePlayer:" + name - // We cannot reverse this, so return the raw UUID - return uuidString; + // We cannot reverse this, so use the fallback name or "N/A" + String name = (offlineFallbackName != null) ? offlineFallbackName : "N/A"; + NAME_CACHE.put(uuidString, name); + return name; } // Online UUID (version 4) - query Mojang