diff --git a/src/main/kotlin/cn/rtast/fancybot/Const.kt b/src/main/kotlin/cn/rtast/fancybot/Const.kt index 59a843e..789ea6b 100644 --- a/src/main/kotlin/cn/rtast/fancybot/Const.kt +++ b/src/main/kotlin/cn/rtast/fancybot/Const.kt @@ -105,5 +105,5 @@ val commands = listOf( QQMusicCommand(), BullShitGenerateCommand(), NiuziRankCommand(), NiuziBankRankCommand(), ShotSelfCommand(), TenSetuCommand(), - ReactionCommand() + ReactionCommand(), MCSkinCommand() ) \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/commands/lookup/MCSkinCommand.kt b/src/main/kotlin/cn/rtast/fancybot/commands/lookup/MCSkinCommand.kt new file mode 100644 index 0000000..190ddc3 --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/commands/lookup/MCSkinCommand.kt @@ -0,0 +1,89 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/10/2 + */ + + +package cn.rtast.fancybot.commands.lookup + +import cn.rtast.fancybot.entity.mc.DecodedSkin +import cn.rtast.fancybot.entity.mc.Skin +import cn.rtast.fancybot.entity.mc.Username +import cn.rtast.fancybot.util.Http +import cn.rtast.fancybot.util.isFullyTransparent +import cn.rtast.fancybot.util.scaleImage +import cn.rtast.fancybot.util.str.decodeToString +import cn.rtast.fancybot.util.str.encodeToBase64 +import cn.rtast.fancybot.util.str.fromJson +import cn.rtast.fancybot.util.toByteArray +import cn.rtast.rob.entity.GroupMessage +import cn.rtast.rob.util.BaseCommand +import cn.rtast.rob.util.ob.MessageChain +import cn.rtast.rob.util.ob.OneBotListener +import java.awt.AlphaComposite +import java.awt.image.BufferedImage +import java.net.URI +import javax.imageio.ImageIO + +class MCSkinCommand : BaseCommand() { + override val commandNames = listOf("/skin") + + private val uuidQueryUrl = "https://api.mojang.com/users/profiles/minecraft" + private val skinUrl = "https://sessionserver.mojang.com/session/minecraft/profile" + + private fun String.isUsername(): Boolean { + val usernamePattern = Regex("^[a-zA-Z][a-zA-Z0-9_]{2,15}$") + return usernamePattern.matches(this) + } + + /** + * 截取整张皮肤中的头部部分, 如果是单层皮肤则直接截取脸部 + * 如果是双层皮肤也就将脸部作为背景图层, 将头发绘制在脸部上 + * 代码来自`YeeeesMOTD` + * `https://github.com/RTAkland/YeeeesMOTD/blob/main/core/src/main/kotlin/cn/rtast/yeeeesmotd/utils/ImageUtil.kt` + */ + private fun getSkinHead(skinUrl: String): String { + val url = URI(skinUrl).toURL() + val image = ImageIO.read(url) + var subImage = image.getSubimage(8, 8, 8, 8) + + if (subImage.isFullyTransparent()) { + subImage = image.getSubimage(40, 8, 8, 8) + } else { + val hairLayer = image.getSubimage(40, 8, 8, 8) + val combined = BufferedImage(subImage.width, subImage.height, BufferedImage.TYPE_INT_ARGB) + val g2d = combined.createGraphics() + g2d.drawImage(subImage, 0, 0, null) + g2d.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f) + g2d.drawImage(hairLayer, 0, 0, null) + g2d.dispose() + subImage = combined + } + val zoom = subImage.scaleImage(128 to 128) + return zoom.toByteArray().encodeToBase64() + } + + override suspend fun executeGroup(listener: OneBotListener, message: GroupMessage, args: List) { + if (args.isEmpty()) { + message.reply("发送`/skin `即可查询他的皮肤~ (仅限Java版)") + return + } + try { + val nameOrUUID = args.first().trim() + val uuid = if (nameOrUUID.isUsername()) Http.get("$uuidQueryUrl/$nameOrUUID").id + else nameOrUUID + val skinUrl = Http.get("$skinUrl/$uuid") + .properties.first().value.decodeToString().fromJson() + .textures.skin.url + val headBase64 = this.getSkinHead(skinUrl) + val msg = MessageChain.Builder() + .addImage(headBase64, true) + .addText(skinUrl) + .build() + message.reply(msg) + } catch (_: Exception) { + message.reply("输入错误, 检查一下是否输入正确吧~") + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/entity/mc/Skin.kt b/src/main/kotlin/cn/rtast/fancybot/entity/mc/Skin.kt new file mode 100644 index 0000000..31dd770 --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/entity/mc/Skin.kt @@ -0,0 +1,31 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/10/2 + */ + + +package cn.rtast.fancybot.entity.mc + +import com.google.gson.annotations.SerializedName + +data class Skin( + val properties: List, +) { + data class Property( + val value: String, + ) +} + +data class DecodedSkin( + val textures: Texture, +) { + data class Texture( + @SerializedName("SKIN") + val skin: SKIN, + ) + + data class SKIN( + val url: String, + ) +} \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/entity/mc/Username.kt b/src/main/kotlin/cn/rtast/fancybot/entity/mc/Username.kt new file mode 100644 index 0000000..f27242a --- /dev/null +++ b/src/main/kotlin/cn/rtast/fancybot/entity/mc/Username.kt @@ -0,0 +1,10 @@ +/* + * Copyright © 2024 RTAkland + * Author: RTAkland + * Date: 2024/10/2 + */ + + +package cn.rtast.fancybot.entity.mc + +data class Username(val id: String) \ No newline at end of file diff --git a/src/main/kotlin/cn/rtast/fancybot/util/Image.kt b/src/main/kotlin/cn/rtast/fancybot/util/Image.kt index cdb8af8..d7dacad 100644 --- a/src/main/kotlin/cn/rtast/fancybot/util/Image.kt +++ b/src/main/kotlin/cn/rtast/fancybot/util/Image.kt @@ -59,7 +59,7 @@ fun Graphics2D.drawCircularImage( maxHeight: Double, ) { val (targetWidth, targetHeight) = image.getScaledWidth(maxWidth, maxHeight) - val circle = Ellipse2D.Double(x.toDouble(), y.toDouble(), targetWidth.toDouble(), targetHeight.toDouble()); + val circle = Ellipse2D.Double(x.toDouble(), y.toDouble(), targetWidth.toDouble(), targetHeight.toDouble()) this.clip = circle this.drawImage(image, x, y, targetWidth, targetHeight, null) this.clip = null @@ -114,4 +114,18 @@ fun Graphics2D.drawString(text: String, x: Int, y: Int, maxWidth: Int) { if (line.isNotEmpty()) { this.drawString(line.toString(), x, curY) } +} + +fun BufferedImage.isFullyTransparent(): Boolean { + val width = this.width + val height = this.height + for (y in 0 until height) { + for (x in 0 until width) { + val pixel = this.getRGB(x, y) + if ((pixel shr 24) != 0x00) { + return false + } + } + } + return true } \ No newline at end of file