离线账户与 UUID 算法
离线账户没有服务器可问,只有一个名字。它的身份完全由名字推导出来——而推导算法必须和所有主流启动器一字不差,否则你在 A 启动器玩的世界存档,到了 B 启动器就是另一个人。
为什么必须手工拼 UUID
Java 侧生成离线 UUID 用的是 UUID.nameUUIDFromBytes,也就是 UUID v3(name-based, MD5):把名字 MD5 一下当哈希,然后把结果整形成 UUID 的样子。
.NET 没有这个 API。Guid 内部是三段 16 位整数 + 8 字节,和 UUID 的字节布局正好错位;而 new Guid(byte[]) 又按 .NET 的小端布局解释这 16 个字节。所以不能直接 new Guid(md5).ToString()——那样得到的字符串字节序是反的。
正确做法是:MD5 出 16 字节 → 手工打上 version 位和 variant 位 → 按大端解释拼成 Guid 的三个整数字段。
public static string OfflineUuid(string username)
{
var bytes = MD5.HashData(Encoding.UTF8.GetBytes("OfflinePlayer:" + (username ?? "")));
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x30); // version = 3(name-based, MD5)
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant = RFC 4122
// 三个多字节字段按大端解释:Java 的 UUID.toString() 就是这样打印哈希字节的
return new Guid(
unchecked((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]),
unchecked((short)((bytes[4] << 8) | bytes[5])),
unchecked((short)((bytes[6] << 8) | bytes[7])),
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15])
.ToString();
}三处细节缺一不可:
- version 位:
bytes[6]的高 4 位清零后置3(0b0011)。不清的话算出来的可能是个 version 0 或 4 的 UUID,格式上就非法。 - variant 位:
bytes[8]的高 2 位清零后置10(0b10),即 RFC 4122 变体。 - 大端序:Java 的
UUID.toString()是把哈希字节按MSB first打印的,.NET的Guid构造器按 little-endian 解释int/short字段,所以要手工移位拼进去。
前缀是 "OfflinePlayer:"(带冒号),大小写敏感。输入是 UTF-8 编码。
别用 SHA-1
SHA-1 算出来的是 v5,Java 那边没有对应的 nameUUIDFromBytes 方法。版本位必须是 3。
迁移
早期版本只存了名字没存 uuid。AccountStore.Load() 里有一条迁移:离线账户缺 uuid 时按名字补算并回写。所以老账户升级后身份不变。
名字校验
离线名字必须 3–16 位,只允许 ASCII 字母、数字和下划线:
public static bool IsValidOfflineName(string username) =>
!string.IsNullOrWhiteSpace(username) && username.Length is >= 3 and <= 16 &&
username.All(c => char.IsAsciiLetterOrDigit(c) || c == '_');这不是我们自己定的规矩,是 Mojang 官方对离线/演示模式名字的限制。放开限制会导致某些服务器在握手阶段直接踢人。
去重按 uuid 走:AddOrUpdateOffline 先算 uuid,再按 uuid 找已有账户。因为算法是纯函数,同一个名字永远映射到同一个 uuid,所以「同名即同人」是自动成立的。
离线的启动身份
离线账户没有任何凭证,启动时用占位令牌:
new LaunchCredentials(off.Uuid, "0", "legacy")access token 是字符串 "0",userType 是 legacy。这只对本地/局域网和离线服务器有效——任何做正版校验的服务端都会拒绝。
离线的皮肤走另一条通道:启动器自己起一个只监听 127.0.0.1 的极简 HTTP 服务托管 PNG,再把 URL 通过 --userProperties 交给客户端。细节在皮肤库与换肤。