Skip to content

Commit f318bdf

Browse files
committed
feat: (skinchanger) fetch skins from Mojang so the module applies the chosen skin
The Online mode only scanned the current server tab list, so it could never load an arbitrary account and did nothing out of the box. Resolve the name to a UUID, pull the skin texture URL from the session server and download it through the vanilla threaded image pipeline (with legacy 64x32 conversion). File mode now runs images through the same legacy converter so non-64x64 and transparent skins render correctly.
1 parent 1331b93 commit f318bdf

1 file changed

Lines changed: 68 additions & 12 deletions

File tree

  • src/main/java/net/ccbluex/liquidbounce/features/module/modules/visual

src/main/java/net/ccbluex/liquidbounce/features/module/modules/visual/SkinChanger.kt

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,24 @@
55
*/
66
package net.ccbluex.liquidbounce.features.module.modules.visual
77

8+
import kotlinx.coroutines.launch
89
import net.ccbluex.liquidbounce.event.UpdateEvent
910
import net.ccbluex.liquidbounce.event.handler
1011
import net.ccbluex.liquidbounce.features.module.Category
1112
import net.ccbluex.liquidbounce.features.module.Module
1213
import net.ccbluex.liquidbounce.file.FileManager.dir
1314
import net.ccbluex.liquidbounce.utils.client.ClientUtils.LOGGER
1415
import 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
1523
import net.minecraft.client.renderer.texture.DynamicTexture
1624
import net.minecraft.util.ResourceLocation
25+
import java.awt.image.BufferedImage
1726
import java.io.File
1827
import 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

Comments
 (0)