From 681b1efa0a6344c2696fb26eb9cc6883a80ad779 Mon Sep 17 00:00:00 2001 From: F1xGOD Date: Sat, 11 Jul 2026 00:57:25 -0700 Subject: [PATCH] Fix offline fallback auth and config generation Avoid offering offline data migration before a premium/Yggdrasil account is verified, so true offline clients can fall back without seeing a premium migration prompt or getting stuck. Move migration confirmation into a second verified-login query that preserves the pending verified profile, then migrates only after explicit confirmation. Fix Forge TOML generation for auth.yggdrasil.apiRootWhitelist to stop repeated config correction backups, and make generated config comments English-first while keeping trueuuid.* translation keys for client-language chat/title text. Honor allowOfflineOnFailure=false and tighten Yggdrasil whitelist validation to host matches instead of substring matches. --- .../alini/trueuuid/config/TrueuuidConfig.java | 39 +-- .../mixin/client/ClientHandshakeMixin.java | 6 + .../mixin/server/ServerLoginMixin.java | 253 +++++++++++++----- .../java/cn/alini/trueuuid/net/NetIds.java | 3 +- .../cn/alini/trueuuid/server/AuthDecider.java | 11 + 5 files changed, 232 insertions(+), 80 deletions(-) diff --git a/src/main/java/cn/alini/trueuuid/config/TrueuuidConfig.java b/src/main/java/cn/alini/trueuuid/config/TrueuuidConfig.java index 57658db..a2b7ae4 100644 --- a/src/main/java/cn/alini/trueuuid/config/TrueuuidConfig.java +++ b/src/main/java/cn/alini/trueuuid/config/TrueuuidConfig.java @@ -71,39 +71,42 @@ public static final class Common { b.push("auth"); timeoutMs = b.defineInRange("timeoutMs", 30_000L, 1_000L, 600_000L); - allowOfflineOnTimeout = b.comment("false: 超时踢出 / Kick on timeout. true: 超时放行为离线 / Allow offline fallback on timeout.").define("allowOfflineOnTimeout", false); - allowOfflineOnFailure = b.comment("false: 失败时踢出 / Kick on failure. true: 鉴权失败放行为离线 / Allow offline fallback on authentication failure.").define("allowOfflineOnFailure", true); + allowOfflineOnTimeout = b.comment("false: kick on timeout. true: allow offline fallback on timeout. / false: 超时踢出。true: 超时允许离线兜底。").define("allowOfflineOnTimeout", false); + allowOfflineOnFailure = b.comment("false: kick on authentication failure. true: allow offline fallback on authentication failure. / false: 鉴权失败时踢出。true: 鉴权失败时允许离线兜底。").define("allowOfflineOnFailure", true); - timeoutKickMessage = b.comment("Kick message on authentication timeout. Use a trueuuid.* translation key for client-side localization, or enter plain text to force a custom server message.") + timeoutKickMessage = b.comment("Kick message on authentication timeout. Use a trueuuid.* translation key for client-side localization, or enter plain text to force a custom server message. / 鉴权超时踢出消息。使用 trueuuid.* 翻译键可按客户端语言显示,也可填写纯文本强制使用服务器消息。") .define("timeoutKickMessage", "trueuuid.disconnect.timeout"); - offlineFallbackMessage = b.define( + offlineFallbackMessage = b.comment("Offline fallback chat message. Keep the default trueuuid.* key to use each player's game language. / 离线兜底聊天提示。保留默认 trueuuid.* 翻译键可按每个玩家的游戏语言显示。") + .define( "offlineFallbackMessage", "trueuuid.chat.offline_fallback" ); - // 默认短、不占屏 (Default short, does not occupy screen) - offlineShortSubtitle = b.define("offlineShortSubtitle", "trueuuid.subtitle.offline"); - onlineShortSubtitle = b.define("onlineShortSubtitle", "trueuuid.subtitle.online"); - showJoinFeedback = b.comment("是否在玩家进服后显示登录状态提示。/ Show join feedback after a player joins. 关闭后不再发送正版/皮肤站/离线/单人模式 Title,也不发送离线兜底聊天提示;不影响皮肤刷新和鉴权逻辑。/ When disabled, no premium/skin-site/offline/single-player Title or offline fallback chat message is sent; authentication and skin refresh are unchanged.") + // Default subtitles are short and localized by the client language. + offlineShortSubtitle = b.comment("Offline fallback subtitle. Keep the default trueuuid.* key to use each player's game language. / 离线兜底副标题。保留默认 trueuuid.* 翻译键可按每个玩家的游戏语言显示。") + .define("offlineShortSubtitle", "trueuuid.subtitle.offline"); + onlineShortSubtitle = b.comment("Premium login subtitle. Keep the default trueuuid.* key to use each player's game language. / 正版登录副标题。保留默认 trueuuid.* 翻译键可按每个玩家的游戏语言显示。") + .define("onlineShortSubtitle", "trueuuid.subtitle.online"); + showJoinFeedback = b.comment("Show join feedback after a player joins. When disabled, no premium/skin-site/offline/single-player title or offline fallback chat message is sent; authentication and skin refresh are unchanged. / 玩家进服后显示登录状态提示。关闭后不再发送正版、皮肤站、离线、单人模式标题,也不发送离线兜底聊天提示;不影响鉴权和皮肤刷新。") .define("showJoinFeedback", true); // 策略项 (Strategy items) - knownPremiumDenyOffline = b.comment("一旦该名字成功验证过正版/皮肤站,后续鉴权失败时禁止以离线身份进入。/ Once a name has been verified as premium/skin-site, deny later offline fallback for that name.") + knownPremiumDenyOffline = b.comment("Once a name has been verified as premium/skin-site, deny later offline fallback for that name. / 一旦该名字成功验证过正版或皮肤站,后续鉴权失败时禁止以离线身份进入。") .define("knownPremiumDenyOffline", true); - allowOfflineForUnknownOnly = b.comment("仅对从未验证为正版/皮肤站的新名字允许离线兜底。/ Only allow offline fallback for names that have never been verified as premium/skin-site.") + allowOfflineForUnknownOnly = b.comment("Only allow offline fallback for names that have never been verified as premium/skin-site. / 仅对从未验证为正版或皮肤站的新名字允许离线兜底。") .define("allowOfflineForUnknownOnly", true); - recentIpGraceEnabled = b.comment("启用“退出后同 IP 短时重连”容错,只在玩家退出后的 TTL 秒内临时沿用上次认证来源。/ Enable short same-IP reconnect grace after logout, reusing the last verified identity only within the TTL window.") + recentIpGraceEnabled = b.comment("Enable short same-IP reconnect grace after logout, reusing the last verified identity only within the TTL window. / 启用退出后同 IP 短时重连容错,只在 TTL 窗口内临时沿用上次认证身份。") .define("recentIpGrace.enabled", true); - recentIpGraceTtlSeconds = b.comment("退出游戏后允许同 IP 容错重连的秒数。默认 10 秒,避免长期误导为皮肤站/正版登录。/ Same-IP grace seconds after logout. Default is 10 seconds to avoid long-lived misleading premium/skin-site identity.") + recentIpGraceTtlSeconds = b.comment("Same-IP grace seconds after logout. Default is 10 seconds to avoid long-lived misleading premium/skin-site identity. / 退出游戏后允许同 IP 容错重连的秒数。默认 10 秒,避免长期误导为正版或皮肤站登录。") .defineInRange("recentIpGrace.ttlSeconds", 10, 1, 60); - debug = b.comment("启用调试日志输出。/ Enable debug logging.").define("debug", false); + debug = b.comment("Enable debug logging. / 启用调试日志输出。").define("debug", false); apiRootWhitelist = b.comment( - "authlib-injector 皮肤站域名白名单。/ Whitelist for authlib-injector skin-site domains.", - "留空(默认)表示信任客户端上报的任何皮肤站 URL。/ Empty by default: trust any skin-site URL reported by the client.", - "配置后,只有 URL 中包含白名单条目的皮肤站才会被接受。/ When configured, only URLs containing a whitelist entry are accepted.", - "例如 / Example: [\"littleskin.cn\", \"skin.example.com\"]" - ).defineListAllowEmpty(List.of("yggdrasil.apiRootWhitelist"), List::of, o -> o instanceof String); + "Whitelist for authlib-injector skin-site domains. / authlib-injector 皮肤站域名白名单。", + "Empty by default: trust any skin-site URL reported by the client. / 留空表示信任客户端上报的任何皮肤站 URL。", + "When configured, only skin-site hosts matching a whitelist entry are accepted. / 配置后,只有主机名匹配白名单条目的皮肤站才会被接受。", + "Example: [\"littleskin.cn\", \"skin.example.com\"] / 示例: [\"littleskin.cn\", \"skin.example.com\"]" + ).defineListAllowEmpty(List.of("yggdrasil", "apiRootWhitelist"), List::of, o -> o instanceof String); b.pop(); } diff --git a/src/main/java/cn/alini/trueuuid/mixin/client/ClientHandshakeMixin.java b/src/main/java/cn/alini/trueuuid/mixin/client/ClientHandshakeMixin.java index 6bbcbf6..988ad43 100644 --- a/src/main/java/cn/alini/trueuuid/mixin/client/ClientHandshakeMixin.java +++ b/src/main/java/cn/alini/trueuuid/mixin/client/ClientHandshakeMixin.java @@ -61,6 +61,12 @@ public abstract class ClientHandshakeMixin { Connection loginConnection = this.connection; int transactionId = packet.getTransactionId(); + if (offlineUpgradeAvailable && NetIds.MIGRATION_CONFIRM_SERVER_ID.equals(serverId)) { + trueuuid$confirmOfflinePlayerDataUpgrade(mc, offlineUuid, offlineDataSummary, "", loginConnection, transactionId); + ci.cancel(); + return; + } + // dev/离线启动常见的占位 token 不可能通过 Mojang 校验,立即回失败,避免登录线程等到服务器超时。 if (trueuuid$isMissingSessionToken(token)) { trueuuid$sendAuthAck(loginConnection, transactionId, false, "", false, true); diff --git a/src/main/java/cn/alini/trueuuid/mixin/server/ServerLoginMixin.java b/src/main/java/cn/alini/trueuuid/mixin/server/ServerLoginMixin.java index 04e0822..82b5806 100644 --- a/src/main/java/cn/alini/trueuuid/mixin/server/ServerLoginMixin.java +++ b/src/main/java/cn/alini/trueuuid/mixin/server/ServerLoginMixin.java @@ -24,6 +24,7 @@ import java.net.InetSocketAddress; import java.net.URI; +import java.util.List; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; @@ -41,6 +42,11 @@ public abstract class ServerLoginMixin { @Unique private String trueuuid$nonce = null; @Unique private long trueuuid$sentAt = 0L; @Unique private boolean trueuuid$offlineUpgradeOffered = false; + @Unique private boolean trueuuid$migrationConfirmation = false; + @Unique private GameProfile trueuuid$pendingVerifiedProfile = null; + @Unique private AuthState.AuthSource trueuuid$pendingAuthSource = null; + @Unique private String trueuuid$pendingAuthDisplayName = null; + @Unique private String trueuuid$pendingIp = null; @Unique private static final java.util.concurrent.ConcurrentHashMap TRUEUUID$MIGRATION_PENDING = new java.util.concurrent.ConcurrentHashMap<>(); @@ -63,24 +69,17 @@ public abstract class ServerLoginMixin { System.out.println("[TrueUUID] 握手 nonce: " + this.trueuuid$nonce + ", txId: " + this.trueuuid$txId); } - PlayerDataMigration.OfflineData offlineData = null; - if (this.gameProfile != null && !TrueuuidRuntime.NAME_REGISTRY.isKnownPremiumName(this.gameProfile.getName())) { - offlineData = PlayerDataMigration.findOfflineData(this.server, this.gameProfile.getName()); - } - if (offlineData != null && this.gameProfile != null && trueuuid$isMigrationPending(this.gameProfile.getName())) { + if (this.gameProfile != null && trueuuid$isMigrationPending(this.gameProfile.getName())) { sendDisconnectWithReason(Component.translatable("trueuuid.disconnect.migration_pending")); reset(); return; } - this.trueuuid$offlineUpgradeOffered = offlineData != null; + this.trueuuid$offlineUpgradeOffered = false; + this.trueuuid$migrationConfirmation = false; FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); buf.writeUtf(this.trueuuid$nonce); - buf.writeBoolean(this.trueuuid$offlineUpgradeOffered); - if (this.trueuuid$offlineUpgradeOffered) { - buf.writeUtf(offlineData.offlineUuid().toString()); - buf.writeUtf(offlineData.summary()); - } + buf.writeBoolean(false); this.connection.send(new ClientboundCustomQueryPacket(this.trueuuid$txId, NetIds.AUTH, buf)); } @@ -186,6 +185,38 @@ public abstract class ServerLoginMixin { if (TrueuuidConfig.debug()) { System.out.println("[TrueUUID] 客户端认证包ackOk: " + ackOk + ", hasJoinedUrl: " + (clientHasJoinedUrl.isEmpty() ? "(mojang default)" : clientHasJoinedUrl)); } + if (this.trueuuid$migrationConfirmation) { + if (this.trueuuid$ackHandled) { + if (TrueuuidConfig.debug()) { + System.out.println("[TrueUUID] 重复迁移确认包忽略, txId: " + this.trueuuid$txId); + } + ci.cancel(); + return; + } + this.trueuuid$ackHandled = true; + try { + if (!ackOk || !finalMigrationConfirmed || this.trueuuid$pendingVerifiedProfile == null || this.trueuuid$pendingAuthSource == null) { + trueuuid$sendDuplicateUuidDisconnect(); + return; + } + + trueuuid$markMigrationPending(this.trueuuid$pendingVerifiedProfile.getName()); + trueuuid$completeVerifiedLogin( + this.trueuuid$pendingIp != null ? this.trueuuid$pendingIp : ip, + this.trueuuid$pendingVerifiedProfile, + this.trueuuid$pendingAuthSource, + this.trueuuid$pendingAuthDisplayName, + true + ); + } finally { + if (this.trueuuid$pendingVerifiedProfile != null) { + trueuuid$clearMigrationPending(this.trueuuid$pendingVerifiedProfile.getName()); + } + reset(); + } + ci.cancel(); + return; + } if (!ackOk) { if (TrueuuidConfig.debug()) { System.out.println("[TrueUUID] 认证失败, 玩家: " + (this.gameProfile != null ? this.gameProfile.getName() : "") + ", ip: " + ip + ", 原因: 客户端拒绝"); @@ -198,18 +229,12 @@ public abstract class ServerLoginMixin { if (!clientHasJoinedUrl.isEmpty()) { var whitelist = TrueuuidConfig.apiRootWhitelist(); if (!whitelist.isEmpty()) { - boolean allowed = false; - for (String entry : whitelist) { - if (clientHasJoinedUrl.contains(entry)) { - allowed = true; - break; - } - } + boolean allowed = trueuuid$isWhitelistedAuthEndpoint(clientHasJoinedUrl, whitelist); if (!allowed) { if (TrueuuidConfig.debug()) { System.out.println("[TrueUUID] 客户端上报的 hasJoined URL 不在白名单中: " + clientHasJoinedUrl); } - handleAuthFailure(ip, "不受信任的认证服务器", false); + handleAuthFailure(ip, "不受信任的认证服务器", false); reset(); ci.cancel(); return; } } @@ -280,16 +305,9 @@ public abstract class ServerLoginMixin { propMap.put(p.name(), new Property(p.name(), p.value())); } } - this.gameProfile = newProfile; - AuthState.markAuthSuccess(this.connection, res.uuid(), res.name(), source, displayName); - if (!trueuuid$handleOfflineUpgradeIfNeeded(res.name(), res.uuid(), source, displayName, finalMigrationConfirmed)) { + if (!trueuuid$completeVerifiedLogin(ip, newProfile, source, displayName, finalMigrationConfirmed)) { return; } - TrueuuidRuntime.NAME_REGISTRY.recordSuccess(res.name(), res.uuid(), ip, source, displayName); - TrueuuidRuntime.IP_GRACE.record(res.name(), ip, res.uuid(), source, displayName); - if (TrueuuidConfig.debug()) { - System.out.println("[TrueUUID] 记录认证成功来源: " + source + ", displayName=" + displayName); - } // 认证成功后只替换档案并释放暂停,后续由 Forge 原版登录 tick 继续完成协商和放入世界。 } catch (Throwable t) { if (TrueuuidConfig.debug()) { @@ -297,10 +315,12 @@ public abstract class ServerLoginMixin { } handleAuthFailure(ip, "服务器异常", false); } finally { - if (this.gameProfile != null) { - trueuuid$clearMigrationPending(this.gameProfile.getName()); + if (!this.trueuuid$migrationConfirmation) { + if (this.gameProfile != null) { + trueuuid$clearMigrationPending(this.gameProfile.getName()); + } + reset(); } - reset(); } }); }); @@ -424,46 +444,152 @@ private void sendDisconnectWithReason(Component reason) { } @Unique - private boolean trueuuid$handleOfflineUpgradeIfNeeded(String name, UUID verifiedUuid, AuthState.AuthSource source, String displayName, boolean confirmed) { - if (!PlayerDataMigration.needsOfflineUpgrade(this.server, name, verifiedUuid)) { - return true; + private boolean trueuuid$completeVerifiedLogin(String ip, GameProfile verifiedProfile, AuthState.AuthSource source, String displayName, boolean migrationConfirmed) { + if (verifiedProfile == null || verifiedProfile.getId() == null || verifiedProfile.getName() == null || verifiedProfile.getName().isBlank()) { + sendDisconnectWithReason(Component.translatable("trueuuid.disconnect.auth_denied")); + return false; } + + String name = verifiedProfile.getName(); + UUID verifiedUuid = verifiedProfile.getId(); PlayerDataMigration.OfflineData data = PlayerDataMigration.findOfflineData(this.server, name); - if (data == null) { - return true; + if (data != null && !data.offlineUuid().equals(verifiedUuid)) { + if (!migrationConfirmed) { + trueuuid$requestOfflineUpgradeConfirmation(data, verifiedProfile, source, displayName, ip); + return false; + } + try { + PlayerDataMigration.migrateOfflineToVerified(this.server, name, verifiedUuid); + if (TrueuuidConfig.debug()) { + System.out.println("[TrueUUID] migrated offline data before login: player=" + name + + ", offlineUuid=" + data.offlineUuid() + ", verifiedUuid=" + verifiedUuid); + } + } catch (Exception ex) { + System.out.println("[TrueUUID] 离线玩家数据继承失败, player=" + name + ", offlineUuid=" + data.offlineUuid() + + ", verifiedUuid=" + verifiedUuid + ", error=" + ex); + sendDisconnectWithReason(Component.translatable( + "trueuuid.disconnect.migration_failed", + name, + data.offlineUuid(), + verifiedUuid, + ex.getMessage() + )); + return false; + } + } + + this.gameProfile = verifiedProfile; + AuthState.markAuthSuccess(this.connection, verifiedUuid, name, source, displayName); + TrueuuidRuntime.NAME_REGISTRY.recordSuccess(name, verifiedUuid, ip, source, displayName); + TrueuuidRuntime.IP_GRACE.record(name, ip, verifiedUuid, source, displayName); + if (TrueuuidConfig.debug()) { + System.out.println("[TrueUUID] 记录认证成功来源: " + source + ", displayName=" + displayName); + } + return true; + } + + @Unique + private void trueuuid$requestOfflineUpgradeConfirmation(PlayerDataMigration.OfflineData data, GameProfile verifiedProfile, AuthState.AuthSource source, String displayName, String ip) { + this.trueuuid$pendingVerifiedProfile = verifiedProfile; + this.trueuuid$pendingAuthSource = source; + this.trueuuid$pendingAuthDisplayName = displayName; + this.trueuuid$pendingIp = ip; + this.trueuuid$migrationConfirmation = true; + this.trueuuid$offlineUpgradeOffered = true; + this.trueuuid$ackHandled = false; + this.trueuuid$txId = TRUEUUID$NEXT_TX_ID.getAndIncrement(); + this.trueuuid$nonce = NetIds.MIGRATION_CONFIRM_SERVER_ID; + this.trueuuid$sentAt = System.currentTimeMillis(); + + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUtf(NetIds.MIGRATION_CONFIRM_SERVER_ID); + buf.writeBoolean(true); + buf.writeUtf(data.offlineUuid().toString()); + buf.writeUtf(data.summary()); + this.connection.send(new ClientboundCustomQueryPacket(this.trueuuid$txId, NetIds.AUTH, buf)); + } + + @Unique + private void trueuuid$sendDuplicateUuidDisconnect() { + if (this.trueuuid$pendingVerifiedProfile == null || this.trueuuid$pendingAuthSource == null) { + sendDisconnectWithReason(Component.translatable("trueuuid.disconnect.auth_denied")); + return; + } + String name = this.trueuuid$pendingVerifiedProfile.getName(); + UUID verifiedUuid = this.trueuuid$pendingVerifiedProfile.getId(); + PlayerDataMigration.OfflineData data = PlayerDataMigration.findOfflineData(this.server, name); + if (data == null || verifiedUuid == null) { + sendDisconnectWithReason(Component.translatable("trueuuid.disconnect.auth_denied")); + return; } - Component sourceName = source == AuthState.AuthSource.YGGDRASIL - ? Component.translatable("trueuuid.auth_source.skin_site.with_name", displayName == null || displayName.isBlank() ? "Yggdrasil" : displayName) + + Component sourceName = this.trueuuid$pendingAuthSource == AuthState.AuthSource.YGGDRASIL + ? Component.translatable("trueuuid.auth_source.skin_site.with_name", + this.trueuuid$pendingAuthDisplayName == null || this.trueuuid$pendingAuthDisplayName.isBlank() ? "Yggdrasil" : this.trueuuid$pendingAuthDisplayName) : Component.translatable("trueuuid.auth_source.premium"); - if (!confirmed) { - sendDisconnectWithReason(Component.translatable( - "trueuuid.disconnect.duplicate_uuid", - sourceName, - data.offlineUuid(), - verifiedUuid, - name - )); + sendDisconnectWithReason(Component.translatable( + "trueuuid.disconnect.duplicate_uuid", + sourceName, + data.offlineUuid(), + verifiedUuid, + name + )); + } + + @Unique + private static boolean trueuuid$isWhitelistedAuthEndpoint(String hasJoinedUrl, List whitelist) { + if (hasJoinedUrl == null || hasJoinedUrl.isBlank()) { return false; } + String host; try { - PlayerDataMigration.migrateOfflineToVerified(this.server, name, verifiedUuid); - if (TrueuuidConfig.debug()) { - System.out.println("[TrueUUID] migrated offline data before login: player=" + name - + ", offlineUuid=" + data.offlineUuid() + ", verifiedUuid=" + verifiedUuid); - } - return true; - } catch (Exception ex) { - System.out.println("[TrueUUID] 离线玩家数据继承失败, player=" + name + ", offlineUuid=" + data.offlineUuid() - + ", verifiedUuid=" + verifiedUuid + ", error=" + ex); - sendDisconnectWithReason(Component.translatable( - "trueuuid.disconnect.migration_failed", - name, - data.offlineUuid(), - verifiedUuid, - ex.getMessage() - )); + URI uri = URI.create(hasJoinedUrl); + host = uri.getHost(); + } catch (Throwable ignored) { + return false; + } + if (host == null || host.isBlank()) { return false; } + String normalizedHost = host.toLowerCase(java.util.Locale.ROOT); + for (String entry : whitelist) { + String allowed = trueuuid$normalizeWhitelistHost(entry); + if (allowed.isEmpty()) { + continue; + } + if (normalizedHost.equals(allowed) || normalizedHost.endsWith("." + allowed)) { + return true; + } + } + return false; + } + + @Unique + private static String trueuuid$normalizeWhitelistHost(String entry) { + if (entry == null || entry.isBlank()) { + return ""; + } + String value = entry.trim().toLowerCase(java.util.Locale.ROOT); + try { + if (value.contains("://")) { + String host = URI.create(value).getHost(); + return host == null ? "" : host.toLowerCase(java.util.Locale.ROOT); + } + } catch (Throwable ignored) { + return ""; + } + while (value.startsWith(".")) { + value = value.substring(1); + } + int slash = value.indexOf('/'); + if (slash >= 0) { + value = value.substring(0, slash); + } + int port = value.indexOf(':'); + if (port >= 0) { + value = value.substring(0, port); + } + return value; } @Unique @@ -475,5 +601,10 @@ private void reset() { this.trueuuid$nonce = null; this.trueuuid$sentAt = 0L; this.trueuuid$offlineUpgradeOffered = false; + this.trueuuid$migrationConfirmation = false; + this.trueuuid$pendingVerifiedProfile = null; + this.trueuuid$pendingAuthSource = null; + this.trueuuid$pendingAuthDisplayName = null; + this.trueuuid$pendingIp = null; } } diff --git a/src/main/java/cn/alini/trueuuid/net/NetIds.java b/src/main/java/cn/alini/trueuuid/net/NetIds.java index 32ad1b7..56fd0bd 100644 --- a/src/main/java/cn/alini/trueuuid/net/NetIds.java +++ b/src/main/java/cn/alini/trueuuid/net/NetIds.java @@ -4,6 +4,7 @@ public final class NetIds { public static final ResourceLocation AUTH = new ResourceLocation("trueuuid", "auth"); + public static final String MIGRATION_CONFIRM_SERVER_ID = "trueuuid:migration-confirm"; private NetIds() {} -} \ No newline at end of file +} diff --git a/src/main/java/cn/alini/trueuuid/server/AuthDecider.java b/src/main/java/cn/alini/trueuuid/server/AuthDecider.java index b3be792..dd06be9 100644 --- a/src/main/java/cn/alini/trueuuid/server/AuthDecider.java +++ b/src/main/java/cn/alini/trueuuid/server/AuthDecider.java @@ -68,12 +68,23 @@ public static Decision onFailure(String name, String ip, boolean explicitOffline return d; } + if (!TrueuuidConfig.allowOfflineOnFailure()) { + d.kind = Decision.Kind.DENY; + d.denyComponent = Component.translatable("trueuuid.disconnect.auth_denied"); + return d; + } + // 3) 未知名字:可允许离线兜底 (Unknown names: Allow offline fallback) if (TrueuuidConfig.allowOfflineForUnknownOnly() && !known) { d.kind = Decision.Kind.OFFLINE; return d; } + if (!TrueuuidConfig.allowOfflineForUnknownOnly()) { + d.kind = Decision.Kind.OFFLINE; + return d; + } + // 4) 否则拒绝 (Otherwise deny) d.kind = Decision.Kind.DENY; d.denyComponent = Component.translatable("trueuuid.disconnect.auth_denied");