[Minecraft]你的皮膚是怎么顯示出來的?皮膚加載與渲染詳解

在MC中,只要是擁有正版的玩家在正版服務器中都能顯示自己獨特的皮膚。那么,這些皮膚是怎么加載與渲染的呢,這篇專欄將會對皮膚機制進行詳解。
目錄
加載皮膚
GameProfile/游戲檔案
Yggdrasil API
應用皮膚
ClientboundPlayerInfoPacket
渲染
如何自定義你的皮膚
正版更換皮膚原理
代理Yggdrasil服務
使用URL定位你的皮膚
一.加載皮膚
1.GameProfile/游戲資料
在MC中,每個玩家都有GameProfile。這個游戲資料的內部存儲了玩家的名稱、UUID和皮膚信息,位于authlib庫中。它的定義如下:
在PropertyMap中包含了我們的皮膚數據。皮膚數據是這個表中的一個Property,它是這樣定義的:
在value中保存的就是我們的皮膚數據,是一個JSON。這個JSON是這樣的:
可以看到,皮膚最后是引用到了一個URL上,這個URL通常是來源于http://textures.minecraft.net/textures/<哈希碼值>
在了解這些后,我們看看GameProfile是如何被序列化的。
GameProfile也可以轉變?yōu)镴SON,它的JSON看起來像這樣(也把PropertyMap的數據寫入了)
玩家可以通過GameProfile得到皮膚的信息,那么MC是如何獲得玩家的游戲數據的呢。接下來就是獲得GameProfile的方法:Yggdrasil API
2.Yggdrsail API
MC采用了一套特殊的API用于玩家賬戶的驗證和獲取數據,這就是Yggdrasil API,位于authlib庫中。這篇專欄主要討論它加載皮膚的部分。
在玩家加入世界后,數據是不完全的,只含有玩家名稱和UUID。這時系統(tǒng)就會調用fillGameProfile填充玩家的數據,補全成為上文那樣。
Yggdrasil API在進行GameProfile數據填充時會向https://sessionserver.mojang.com/profile/<玩家UUID>發(fā)送一個GET請求,附加參數unsigned代表需不需要進行簽名。在通常情況下,requireSecure都為true,也就是會請求簽名。GET請求的結果是一個JSON文本,內容就是上文提到的GameProfile的序列化JSON。通過這個JSON,系統(tǒng)就能解析出玩家的資料。
獲取皮膚使用了另一個方法:getTextures,它能通過玩家的資料提取出皮膚材質信息:
從這段代碼中,我們可以看到皮膚的成功加載由下面幾個條件:
1)這個皮膚數據必須是有簽名的,否則不進行解析
2)找到文件簽名后,就用Yggdrasil API的公鑰進行解密。這個公鑰在MC的文件中,位于/yggdrasil_session_pubkey.der。解密之后的文本應該與value相同,否則驗證失敗
3)進行解析,提取出URL。如果URL不屬于白名單上的域名(這些在白名單的域名有".minecraft.net"和".mojang.com"),那么也不會通過驗證
在三重驗證之后,就得到了一個皮膚的映射,可以讓客戶端下載并使用。
但是,填充GameProfile是服務端的操作,而getTexture是客戶端操作,客戶端要怎么獲取填充好的GameProfile呢?
二.應用皮膚
1.ClientboundPlayerInfoPacket
緊接著上文,客戶端需要服務端的已經填充好的GameProfile,那么服務端就要發(fā)送一個數據包向客戶端通知客戶端獲得數據,這個數據包就是ClientboundPlayerInfoPacket。
這個數據包一共有5種模式:ADD_PLAYER(玩家加入),UPDATE_GAME_MODE(改變游戲模式),UPDATE_LATENCY,UPDATE_DISPLAY_NAME(顯示名稱改變),REMOVE_PLAYER(玩家移除)。有關于GameProfile的傳輸是在ADD_PLAYER內部的。
在玩家加入服務器后,服務器會把所有玩家(包括自身)的GameProfile通過ADD_PLAYER模式發(fā)送到客戶端。
在這個數據包傳到客戶端后,客戶端的監(jiān)聽器將處理這個數據包:
GameProfile的數據會在這時傳入PlayerInfo中,之后渲染系統(tǒng)會訪問這個對象獲取皮膚信息。先看看渲染系統(tǒng)是怎么編寫的。
2.渲染
玩家的渲染在PlayerRenderer中,它通過AbstractClientPlayer的方法獲取皮膚數據,下面是一個例子:
那么AbstractClientPlayer是哪里來的呢?答案還是在客戶端數據包監(jiān)聽器內部。
在進入服務器時,服務器會把所有玩家GameProfile發(fā)送到客戶端。而當其他玩家進入本玩家可視范圍內時,服務端會再發(fā)送一個ClientboundAddPlayerPacket用于通知。通過這個數據包,客戶端可以添加這個玩家對象。
PlayerInfo在這時就傳入到了RemotePlayer(AbstractClientPlayer的子類)中。當渲染器渲染玩家時,調用getSkinTextureLocation,進而調用PlayerInfo的getSkinLocation。如果沒有下載皮膚材質,調用皮膚下載器,使用GameProfile中的URL地址下載到皮膚。
如果皮膚材質加載失敗了,那么系統(tǒng)會自動使用Steve/Alex皮膚。使用這兩個默認皮膚不是隨機的,它有關于玩家UUID的哈希碼值。
至此,你的皮膚就展示在你的屏幕上了!
三.如何自定義你的皮膚
1.正版更換皮膚原理
官方啟動器和很多第三方啟動器都支持更換皮膚,它們是通過API進行換膚操作。

這個API的基礎網址是https://api.mojang.com/user/profile/<玩家UUID>/skin,支持三種操作:上傳皮膚,改變皮膚,重置皮膚。注意:修改皮膚只能在已登錄條件下執(zhí)行,因為這些操作都需要提供accessToken(訪問令牌)
1)上傳皮膚
請求為PUT,Headers需要包含“Authorization”一項,值為“Bearer <訪問令牌>”
負載數據有兩項:
????"model"-值為slim或者為空字符串,slim代表Alex模型,空字符串代表Steve模型
????"file"-負載數據為圖片
2)改變皮膚
請求為POST,Headers要求與第一項相同
在URL上負載數據“model”和“url”,分別為模型和材質地址。材質地址只能屬于第一節(jié)所述的幾個網站,否則不會成功換膚
返回值正常為空負載
3)重置皮膚
請求為DELETE,Headers要求與第一項相同,無負載
2.代理Yggdrasil服務
對于沒有正版賬號的玩家來說,無法更換皮膚,但是如果將Yggdrasil服務換成自己的,不就能用自己的皮膚了嗎?這就是“皮膚站”和“外置登錄”的來源。

實現(xiàn)這個功能,就必須在啟動MC時加入一個Java Agent進行代碼修改,現(xiàn)在大家使用的注入器就是authlib-injector,它能修改Yggdrasil API的底層代碼,使Yggdrasil服務定位到皮膚站的API上,這樣就完成了代理。
(注:這種操作也讓登錄定位到了新的API上,所以MC系統(tǒng)會把你識別為“正版玩家”)
3.使用URL定位你的皮膚
根據Yggdrasil API的原理,可以看到我們可以只修改來源于服務端的GameProfile中的textures屬性數據同時撤銷客戶端的Yggdrasil API的三重檢查來達到重新定位皮膚的目的。
可是怎么修改呢?
首先是服務端,服務端在online-mode為false時會自動調用ServerLoginPacketListenerImpl#createFakeProfile生成一個“假的”游戲資料,,UUID的生成與名稱有關:
這個對象中不包含textures屬性。我們可以在這里插入代碼使它加上數據,這樣傳送到客戶端的數據就包含了skin屬性。(注意,此處不要生成signature,因為沒有私鑰生成不了)
另一部分是客戶端,我們只需要撤銷掉三重檢查就可以達到目的了。由于Mixin無法修改authlib庫內部的類,所以我們可以采用繼承+代理這個類更換掉Minecraft類中的對象。
這樣,就可以自定義你的皮膚了?。▽εL一樣管用,只是多加了一點代碼而已)

代碼來源:1.18_experiment-snapshot-1,Mojang Mapping,使用Yarn Mapping的類名及方法名可能不同,但是邏輯一樣
代碼反混淆器及反編譯器:MCDynamicExchanger beta8,bug正在修復
Mojang API的用法:https://c4k3.github.io/wiki.vg/Mojang_API.html