离线皮肤适配原理
众所周知,在22w45a以上版本,Mojang更新了9种离线皮肤,其中包含了Sunny、Efe等名字的皮肤。然后我们应该如何适配这个新特性呢?
在此之前,我觉得我有必要来和各位启动器开发者们说一件事:
Mojang给我们提供了一种进入游戏时验证玩家身份唯一性的UUID值,如果进入同一个世界,但是UUID不一致,可能就会导致物品栏内的物品都不见。
如果进入的是正版服务器,则可能会导致玩家的所有道具全部删除等状态。即使如此,我们还会发现,那么如果是离线服务器呢?
离线服务器是根据玩家的UserName来在服务端创建一个ServerUUID,只要这个服务器不重启,或是不关闭,则无论玩家在哪台机子使用任意的客户端UUID,当使用该名称时,总是会获取玩家上次背包中的所有物品。
还有一件事,那就是正版用户使用的UUID,这个UUID由Microsoft独立提供的一个身份验证的UUID,也就是说这个UUID为你独特的UUID,所有人无法与你的重合。
那假如我使用离线登录,但是我的UUID填入某个正版用户的UUID,那该怎么办呢?
很简单,你会获取到该用户的正版皮肤与正版披风,仅此而已。我们先来看看PCL2是如何通过玩家正版用户名获取到正版皮肤的!
点击PCL2的设置 -> 游戏 -> 离线皮肤 -> 正版皮肤单选框,然后在里面输入【Zi__Min】,现在再回到你的PCL2主界面,选择离线登录,随便输入一个用户名,然后此时,你们会不会发现,自己的头像被更改啦??
这就是Mojang对于UUID这个对于全球玩家唯一的统一标识符所代表的重要性了吧!
还有一种特殊的,那就是第三方外置登录,Authlib-Injector,这个是通过劫持Mojang与正版服务器的通讯,将其导入进自己的服务器里进行识别皮肤的,因此里面的UUID其实并不作为Mojang官方服务器里的UUID。这个与我们下面教的无关。
下面我们开始正式教如何适配离线皮肤
我们可以在我们的离线登录界面里设置一个下拉框,这个下拉框里面有【随机、Steve、Alex】等一堆设置,当然,粗细手臂的玩家我们也要设置噢!
具体如何操控UUID可以看看我发的这篇帖子,然后看看tdiant是怎么回答的!
通过上面那篇帖子,我们很容易可以看出,Mojang通过解析UUID将其转成一个hashCode,随后通过某种特殊的解析,解析出Minecraft的9种离线皮肤。
我们可以写一个这样的函数,用于将玩家输入的UUID转换成hashCode,当然,此处如果是专门使用Java制作启动器的作者们可以跳过这一段。我这里所针对的是【在自带的语言中没有UUID.hashCode这个方法的】。
我们用Delphi写一个下列函数
//用UUID强转成HashCode。(接受一个参数为UUID,类型是String)
function UUIDToHashCode(UUID: string): Int64; //返回一个long类型的数据,我这里用Int64代替。
begin
result := -1; //先设置返回值为-1
if TRegex.IsMatch(UUID, '^[a-f0-9]{32}') then begin //这里先使用正则表达式,判断输入的UUID是否符合预期。如果符合,则往下执行,反之直接返回-1。
var most := UUID.Substring(0, 16); //将该UUID从中间切开,第一个为UUID的前半部分。
var least := UUID.Substring(16, 16); //这个为UUID的下半部分。
var mostbin := ''; //设置一个前半部分的。
var leastbin := ''; //设置一个后半部分的。
for var I in most do begin //用forEach对UUID前半部分遍历。
if I = '0' then mostbin := mostbin + '0000' //如果循环到0,则mostbin加一个0000
else if I = '1' then mostbin := mostbin + '0001' //以下亦然
else if I = '2' then mostbin := mostbin + '0010'
else if I = '3' then mostbin := mostbin + '0011'
else if I = '4' then mostbin := mostbin + '0100'
else if I = '5' then mostbin := mostbin + '0101'
else if I = '6' then mostbin := mostbin + '0110'
else if I = '7' then mostbin := mostbin + '0111'
else if I = '8' then mostbin := mostbin + '1000'
else if I = '9' then mostbin := mostbin + '1001'
else if I = 'a' then mostbin := mostbin + '1010'
else if I = 'b' then mostbin := mostbin + '1011'
else if I = 'c' then mostbin := mostbin + '1100'
else if I = 'd' then mostbin := mostbin + '1101'
else if I = 'e' then mostbin := mostbin + '1110'
else if I = 'f' then mostbin := mostbin + '1111'
end;
for var I in least do begin //这里遍历的是后半部分。
if I = '0' then leastbin := leastbin + '0000'
else if I = '1' then leastbin := leastbin + '0001'
else if I = '2' then leastbin := leastbin + '0010'
else if I = '3' then leastbin := leastbin + '0011'
else if I = '4' then leastbin := leastbin + '0100'
else if I = '5' then leastbin := leastbin + '0101'
else if I = '6' then leastbin := leastbin + '0110'
else if I = '7' then leastbin := leastbin + '0111'
else if I = '8' then leastbin := leastbin + '1000'
else if I = '9' then leastbin := leastbin + '1001'
else if I = 'a' then leastbin := leastbin + '1010'
else if I = 'b' then leastbin := leastbin + '1011'
else if I = 'c' then leastbin := leastbin + '1100'
else if I = 'd' then leastbin := leastbin + '1101'
else if I = 'e' then leastbin := leastbin + '1110'
else if I = 'f' then leastbin := leastbin + '1111'
end;
//此时,mostbin和leastbin就是该UUID前半部分与后半部分的二进制,
var xor1 := ''; //设立一个临时变量。
for var I := 1 to mostbin.Length do begin //这个循环是对上述两个进行xor计算。
//具体xor是什么,请自行百度。
if mostbin[I] = leastbin[I] then begin
xor1 := xor1 + '0';
end else begin
xor1 := xor1 + '1';
end;
end;
var mostx := xor1.Substring(0, 32); //这里对第一次xor出来的值再次切割成两半。
var leastx := xor1.Substring(32, 32);
var xor2 := ''; //再设立一个临时变量。
for var I := 1 to mostx.Length do begin //再做一次xor计算。
if mostx[I] = leastx[I] then begin
xor2 := xor2 + '0';
end else begin
xor2 := xor2 + '1';
end;
end;
var ten: Int64 := 0; //最后将该二进制数据转换成10进制,用long类型的接收。
for var I := 1 to xor2.Length do begin
if xor2[I] = '1' then begin
ten := ten + Trunc(IntPower(2, xor2.Length - I)); //这里可能写的有点高血压,不过也差不多。。
end;
end;
result := ten; //将最后的10进制当作返回值返回。
end;
end;
好了,那么上面的UUIDToHashCode,只是一个将UUID转换成Int数字的一事。但是对于一个UUID的可能性远远比long类型所能装得下的数据来说,远远的超过了,因此总有至少n个UUID转换成long类型的数据是一样的。这点不可否认。
随后,我们来看看我们应该如何通过这个函数,来生成一个我们自己的皮肤解析吧!
首先,我们进入AccountForm这个类,然后我们新建一个函数,名为UUIDToAvatar,这个的意思就是当我们选中下拉框中的任意元素时,所可能触发的事件。
先在我们自定义的下拉框里设置这么几个元素【可以选择按照顺序,也可以选择不按照】:
alex-slim
ari-slim
efe-slim
kai-slim
makena-slim
noor-slim
steve-slim
sunny-slim
zuri-slim
alex-bold
ari-bold
efe-bold
kai-bold
makena-bold
noor-bold
steve-bold
sunny-bold
zuri-bold
其中,上述对应的,就是一个顺序。从alex开始,一直往下,所得到的index,就是我们的hashCode除以18得到的【余数】所对应的index。
我们直接开始上代码吧:
//根据序号获取UUID。
function TForm3.AvatarToUUID(num: Integer): String;
var
uid: TGuid; //首先置一个UUID的变量。
begin
while true do begin //使用循环遍历通过的num是否符合UUID。
CreateGuid(uid); //为UUID初始化。
var str := GuidToString(uid).Replace('{', '').Replace('}', '').Replace('-', '').ToLower;
//这里使用的是使用UUIDToHashCode对18进行取余操作,如果取出的余数为选择的num,则直接返回str,如果不是,则继续循环。
if (UUIDToHashCode(uuid) mod 18) = num then begin
result := str;
break;
end;
//如果不是,则继续循环。
end;
end;
好了,那么上述就是通过序号获取UUID。我们设立了一个参数,这个参数是Integer类型的,当然,我们是使用了UUIDToHashCode对18进行取余的。
大多数的编程语言使用的应该是【%】这个符号进行取余。但是Delphi是用mod进行取余的,各位不必在意这些细节。
那么我们应该如何使用这个AvatarToUUID呢?
很简单,我们看看下拉框改变事件:
procedure TForm2.ComboBox2Change(Sender: TObject);
begin
//这个Edit2指的是当初我们置下UUID那个输入框的。
Edit2.Text := AvatarToUUID(ComboBox2.ItemIndex);
end;
随后,符合玩家选择的皮肤的UUID就正式被我们获取到了!我们只需要点击添加账号,将UUID保存到外部文件,随后我们启动游戏试试吧!!
下面开始教大家如何通过正版用户名来获取正版用户的UUID。
我们很容易的就可以从wiki.vg里面,找到有关于mojang API的部分解说,其中有一段是通过api.mojang.com里面,通过正版用户的用户名来获取正版的UUID的。
但是此时,我们会发现一个问题,我们无法获取用户的大头像,这里仅仅只是获取到正版用户的UUID,也就是说我们必须得进入游戏之后才能看到自己获取的用户皮肤。
那么我们有什么API可以解决这件事吗?答案是有的!
我们可以通过playerdb来获取正版用户的UUID以及大头像。
我就用我自己的正版用户名来给大家做个示例吧:
GET https://playerdb.co/api/player/minecraft/rechalow
GET https://playerdb.co/api/player/minecraft/<正版用户名或正版UUID>
这个playerdb网址不仅支持正版用户名输入,而且支持正版用户的UUID输入,也就是说如果对方只告诉了你UUID而不告诉你正版用户名,你依旧可以查询得到。
{
"code": "player.found",
"message": "Successfully found player by given ID.",
"data": {
"player": {
"meta": {
"cached_at": 1697884012
},
"username": "Rechalow",
"id": "9b319bd5-2291-4c04-9017-60be25cf0331",
"raw_id": "9b319bd522914c04901760be25cf0331",
"avatar": "https://crafthead.net/avatar/9b319bd522914c04901760be25cf0331",
"name_history": []
}
},
"success": true
}
我们很容易的就可以发现,这里面有我们的基本用户信息。
我们也可以把上述API地址换成如下:
GET https://playerdb.co/api/player/minecraft/9b319bd522914c04901760be25cf0331
返回结果与上述一致。
我们来挨个辨析里面的所有参数:
键值 | 描述 |
---|---|
code | API返回代码,通常用于辨认此时用户获取到的是什么。 |
message | API返回描述信息,用于描述此时获取到了什么。 |
success | 是否返回成功。通常用个if判断,如果API库里面没有这个正版用户名,则返回false |
data | 返回的用户数据对象。 |
data.player | 玩家数据 |
data.player.meta | 玩家元数据,(我不知道里面是什么东西,如果有知道的,可以给我发issue。) |
data.player.username | 玩家用户名 |
data.player.id | 玩家真实的UUID |
data.player.raw_id | 玩家的无符号UUID |
data.player.avatar | 玩家的大头像历史 |
data.player.name_history | 玩家的历史用户名【由于Mojang在API里去除了at时间戳,因此这个作废。】 |
上面的表格清晰的指出了上述API的所有返回信息是怎样的。
下一步,我们将继续来看看如何在代码中获取上述API。【请注意,上述API确实为mojang公开的API,但并不代表玩家可以随时随地查看别的正版用户所更换的皮肤,请酌情适用。】
首先,我们在离线登录旁边置一个按钮。名称为:【通过正版用户名称获取正版用户皮肤】,当然大家也可以自行选择如何置这个按钮。随后敲以下代码:
由于篇幅关系,这里不再赘述如何直接获取正版用户大头像了,大家只需要知道该大头像是180x180像素的,大家自己用自己的语言内置的图像解析器来编写获取用户大头像吧!
procedure <名称随便乱取啦……我也忘了是按钮几号啦……>();
begin
var a := GetWebText('https://playerdb.co/api/player/minecraft/' + Edit1.Text);
//先获取网址里的内容。
var uuid := (((TJSONObject.ParseJSONValue(a) as TJSONObject)
.GetValue('data') as TJSONObject)
.GetValue('player') as TJSONObject)
.GetValue('raw_id').Value;
Edit2.Text := uuid;
end;
欸对,也就是这么简单。如果你还想有更高深的玩法的话,可以在窗口上设置一个图片框,随后在玩家离线登录的同时,获取该用户的大头像。【请记住,这个地方是通过用户的UUID来获取头像,而不是仅仅通过用户名来获取,因为Minecraft最终解析皮肤不是通过Username来解析的。】,如果success为true。则将该大头像放入图片框。如果success为false,则按照默认9种离线皮肤为玩家创建大头像。
具体如何置图片框,这里不多赘述,大家自行准备!好了,话就说到这里,开始下一章吧!
请注意!上述方式仅适用于单人游戏,作者不保证该种方式是否可以用于多人联机。如果你在与朋友联机的时候,使用了上述自建的UUID方式,而未能正确获取到正确的正版皮肤,则不是作者的问题!