55 */
66package net.ccbluex.liquidbounce.features.module.modules.visual
77
8+ import kotlinx.coroutines.launch
89import net.ccbluex.liquidbounce.event.UpdateEvent
910import net.ccbluex.liquidbounce.event.handler
1011import net.ccbluex.liquidbounce.features.module.Category
1112import net.ccbluex.liquidbounce.features.module.Module
1213import net.ccbluex.liquidbounce.file.FileManager.dir
1314import net.ccbluex.liquidbounce.utils.client.ClientUtils.LOGGER
1415import net.ccbluex.liquidbounce.utils.client.chat
16+ import net.ccbluex.liquidbounce.utils.io.HttpClient
17+ import net.ccbluex.liquidbounce.utils.io.get
18+ import net.ccbluex.liquidbounce.utils.io.readJson
19+ import net.ccbluex.liquidbounce.utils.kotlin.SharedScopes
20+ import net.minecraft.client.renderer.IImageBuffer
21+ import net.minecraft.client.renderer.ImageBufferDownload
22+ import net.minecraft.client.renderer.ThreadDownloadImageData
1523import net.minecraft.client.renderer.texture.DynamicTexture
1624import net.minecraft.util.ResourceLocation
25+ import java.awt.image.BufferedImage
1726import java.io.File
1827import javax.imageio.ImageIO
1928
@@ -40,11 +49,15 @@ object SkinChanger : Module("SkinChanger", Category.VISUAL, Category.SubCategory
4049 private var resolved: ResourceLocation ? = null
4150 private var lastKey = " "
4251
52+ @Volatile
53+ private var pendingKey = " "
54+
4355 val skinLocation: ResourceLocation ?
4456 get() = if (handleEvents()) resolved else null
4557
4658 private fun invalidate () {
4759 lastKey = " "
60+ pendingKey = " "
4861 resolved = null
4962 }
5063
@@ -70,26 +83,68 @@ object SkinChanger : Module("SkinChanger", Category.VISUAL, Category.SubCategory
7083 return
7184 }
7285
73- if (lastKey == " online:$name " && resolved != null ) {
86+ val key = " online:$name "
87+ if (lastKey == key) {
7488 return
7589 }
7690
77- lastKey = " online:$name "
91+ lastKey = key
92+ pendingKey = key
7893
79- val playerInfoMap = mc.netHandler?.playerInfoMap ? : run {
80- resolved = null
81- return
82- }
94+ SharedScopes . IO .launch {
95+ runCatching {
96+ val uuid = fetchUuid(name) ? : return @launch
97+ val skinUrl = fetchSkinUrl(uuid) ? : return @launch
8398
84- val info = synchronized(playerInfoMap) {
85- ArrayList (playerInfoMap)
86- }.firstOrNull {
87- it?.gameProfile?.name.equals(name, ignoreCase = true )
99+ mc.addScheduledTask {
100+ if (pendingKey != key) return @addScheduledTask
101+ applyDownloadedSkin(key, uuid, skinUrl)
102+ }
103+ }.onFailure {
104+ LOGGER .error(" Failed to resolve skin for $name " , it)
105+ }
88106 }
107+ }
89108
90- resolved = info?.locationSkin
109+ private fun applyDownloadedSkin (key : String , uuid : String , skinUrl : String ) {
110+ val location = ResourceLocation (" fdp/skin-changer/$uuid " )
111+ val texture = ThreadDownloadImageData (
112+ File (skinDir, " $uuid .png" ),
113+ skinUrl,
114+ null ,
115+ object : IImageBuffer {
116+ override fun parseUserSkin (image : BufferedImage ? ): BufferedImage ? =
117+ runCatching { ImageBufferDownload ().parseUserSkin(image) }.getOrNull() ? : image
118+
119+ override fun skinAvailable () {
120+ mc.addScheduledTask { if (pendingKey == key) resolved = location }
121+ }
122+ }
123+ )
124+ mc.textureManager.loadTexture(location, texture)
91125 }
92126
127+ private fun fetchUuid (name : String ): String? =
128+ HttpClient .get(" https://api.mojang.com/users/profiles/minecraft/$name " ).use { response ->
129+ if (! response.isSuccessful) return null
130+ response.body.charStream().readJson().asJsonObject.get(" id" )?.asString?.takeIf (String ::isNotBlank)
131+ }
132+
133+ private fun fetchSkinUrl (uuid : String ): String? =
134+ HttpClient .get(" https://sessionserver.mojang.com/session/minecraft/profile/$uuid " ).use { response ->
135+ if (! response.isSuccessful) return null
136+ val properties = response.body.charStream().readJson().asJsonObject
137+ .getAsJsonArray(" properties" ) ? : return null
138+ val encoded = properties.firstOrNull {
139+ it.asJsonObject.get(" name" )?.asString == " textures"
140+ }?.asJsonObject?.get(" value" )?.asString ? : return null
141+ val decoded = String (java.util.Base64 .getDecoder().decode(encoded), Charsets .UTF_8 )
142+ decoded.reader().readJson().asJsonObject
143+ .getAsJsonObject(" textures" )
144+ ?.getAsJsonObject(" SKIN" )
145+ ?.get(" url" )?.asString?.takeIf (String ::isNotBlank)
146+ }
147+
93148 private fun resolveFile () {
94149 val target = fileName.trim()
95150 if (target.isEmpty()) {
@@ -119,7 +174,8 @@ object SkinChanger : Module("SkinChanger", Category.VISUAL, Category.SubCategory
119174 return
120175 }
121176
122- mc.textureManager.loadTexture(fileSkinLocation, DynamicTexture (image))
177+ val processed = runCatching { ImageBufferDownload ().parseUserSkin(image) }.getOrNull() ? : image
178+ mc.textureManager.loadTexture(fileSkinLocation, DynamicTexture (processed))
123179 resolved = fileSkinLocation
124180 }.onFailure {
125181 LOGGER .error(" Failed to load custom skin" , it)
0 commit comments