From c82c051704424763c20742b616228cfe636b9f65 Mon Sep 17 00:00:00 2001 From: nea Date: Sat, 9 Sep 2023 04:50:29 +0200 Subject: Add custom textures to placed skulls --- docs/Texture Pack Format.md | 12 ++++- .../mixins/MixinSkullBlockEntityRenderer.java | 25 ++++++++++ src/main/kotlin/moe/nea/firmament/Firmament.kt | 1 + .../nea/firmament/features/debug/PowerUserTools.kt | 31 ++++++++++++ .../features/texturepack/CustomSkyBlockTextures.kt | 55 +++++++++++++++++++++- .../nea/firmament/util/IdentityCharacteristics.kt | 17 +++++++ src/main/kotlin/moe/nea/firmament/util/MC.kt | 23 +++++++++ .../moe/nea/firmament/util/item/SkullItemData.kt | 23 +++++++-- .../resources/assets/firmament/lang/en_us.json | 4 ++ 9 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/MixinSkullBlockEntityRenderer.java create mode 100644 src/main/kotlin/moe/nea/firmament/util/IdentityCharacteristics.kt diff --git a/docs/Texture Pack Format.md b/docs/Texture Pack Format.md index bd06b33..587bcd3 100644 --- a/docs/Texture Pack Format.md +++ b/docs/Texture Pack Format.md @@ -8,11 +8,19 @@ SPDX-License-Identifier: CC0-1.0 ## Items by internal id (ExtraAttributes) -Find the internal id of the item. This is usually stored in the ExtraAttributes tag (i will soon-ish add a command for -finding the texture pack id specifically). Once you found it, create an item model in a resource pack like you would for +Find the internal id of the item. This is usually stored in the ExtraAttributes tag (Check the Power User Config for +keybinds). Once you found it, create an item model in a resource pack like you would for a vanilla item model, but at the coordinate `firmskyblock:`. So for an aspect of the end, this would be `firmskyblock:models/item/aspect_of_the_end.json` (or `assets/firmskyblock/models/item/aspect_of_the_end.json`). Then, just use a normal minecraft item model. See https://github.com/romangraef/BadSkyblockTP/blob/master/assets/firmskyblock/models/item/magma_rod.json as an example. +## (Placed) Skulls by texture id + +Find the texture id of a skull. This is the hash part of an url like +`https://textures.minecraft.net/texture/bc8ea1f51f253ff5142ca11ae45193a4ad8c3ab5e9c6eec8ba7a4fcb7bac40` (so after the +/texture/). You can find it in game for placed skulls using the keybinding in the Power User Config. Then place the +replacement texture at `firmskyblock:textures/placedskulls/.png`. Keep in mind that you will probably replace +the texture with another skin texture, meaning that skin texture has it's own hash. Do not mix those up, you need to use +the hash of the old skin. diff --git a/src/main/java/moe/nea/firmament/mixins/MixinSkullBlockEntityRenderer.java b/src/main/java/moe/nea/firmament/mixins/MixinSkullBlockEntityRenderer.java new file mode 100644 index 0000000..7f54150 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/MixinSkullBlockEntityRenderer.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2023 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.mixins; + +import com.mojang.authlib.GameProfile; +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures; +import net.minecraft.block.SkullBlock; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(SkullBlockEntityRenderer.class) +public class MixinSkullBlockEntityRenderer { + @Inject(method = "getRenderLayer", at = @At("HEAD"), cancellable = true) + private static void onGetRenderLayer(SkullBlock.SkullType type, GameProfile profile, CallbackInfoReturnable cir) { + CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir); + } +} diff --git a/src/main/kotlin/moe/nea/firmament/Firmament.kt b/src/main/kotlin/moe/nea/firmament/Firmament.kt index 2ba54a8..6a6007e 100644 --- a/src/main/kotlin/moe/nea/firmament/Firmament.kt +++ b/src/main/kotlin/moe/nea/firmament/Firmament.kt @@ -60,6 +60,7 @@ object Firmament { val json = Json { prettyPrint = DEBUG + isLenient = true ignoreUnknownKeys = true encodeDefaults = true } diff --git a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt index 398042d..52834f3 100644 --- a/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt +++ b/src/main/kotlin/moe/nea/firmament/features/debug/PowerUserTools.kt @@ -6,17 +6,24 @@ package moe.nea.firmament.features.debug +import net.minecraft.block.SkullBlock +import net.minecraft.block.entity.SkullBlockEntity import net.minecraft.item.ItemStack import net.minecraft.text.Text +import net.minecraft.util.hit.BlockHitResult +import net.minecraft.util.hit.HitResult import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.events.ScreenOpenEvent import moe.nea.firmament.events.TickEvent +import moe.nea.firmament.events.WorldKeyboardEvent import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.util.ClipboardUtils +import moe.nea.firmament.util.MC import moe.nea.firmament.util.skyBlockId object PowerUserTools : FirmamentFeature { @@ -28,6 +35,7 @@ object PowerUserTools : FirmamentFeature { val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id") val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data") + val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture") } override val config @@ -55,6 +63,29 @@ object PowerUserTools : FirmamentFeature { lastCopiedStackViewTime = true it.lines.add(text) } + WorldKeyboardEvent.subscribe { + if (it.matches(TConfig.copySkullTexture)) { + val p = MC.camera ?: return@subscribe + val blockHit = p.raycast(20.0, 0.0f, false) ?: return@subscribe + if (blockHit.type != HitResult.Type.BLOCK || blockHit !is BlockHitResult) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return@subscribe + } + val blockAt = p.world.getBlockState(blockHit.blockPos)?.block + val entity = p.world.getBlockEntity(blockHit.blockPos) + if (blockAt !is SkullBlock || entity !is SkullBlockEntity || entity.owner == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + return@subscribe + } + val id = CustomSkyBlockTextures.getSkullTexture(entity.owner!!) + if (id == null) { + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail")) + } else { + ClipboardUtils.setTextContent(id.toString()) + MC.sendChat(Text.translatable("firmament.tooltip.copied.skull", id.toString())) + } + } + } TickEvent.subscribe { if (!lastCopiedStackViewTime) lastCopiedStack = null diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt index b086811..66c0987 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -6,11 +6,21 @@ package moe.nea.firmament.features.texturepack +import com.mojang.authlib.GameProfile +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import net.minecraft.block.SkullBlock +import net.minecraft.client.MinecraftClient +import net.minecraft.client.render.RenderLayer +import net.minecraft.client.texture.PlayerSkinProvider import net.minecraft.client.util.ModelIdentifier +import net.minecraft.util.Identifier import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.gui.config.ManagedConfig +import moe.nea.firmament.util.IdentityCharacteristics +import moe.nea.firmament.util.item.decodeProfileTextureProperty import moe.nea.firmament.util.skyBlockId object CustomSkyBlockTextures : FirmamentFeature { @@ -19,6 +29,7 @@ object CustomSkyBlockTextures : FirmamentFeature { object TConfig : ManagedConfig(identifier) { val enabled by toggle("enabled") { true } + val skullsEnabled by toggle("skulls-enabled") { true } val cacheDuration by integer("cache-duration", 0, 20) { 1 } } @@ -32,8 +43,50 @@ object CustomSkyBlockTextures : FirmamentFeature { it.overrideModel = ModelIdentifier("firmskyblock", id.identifier.path, "inventory") } TickEvent.subscribe { - if (it.tickCount % TConfig.cacheDuration == 0) + if (it.tickCount % TConfig.cacheDuration == 0) { CustomItemModelEvent.clearCache() + skullTextureCache.clear() + } } } + + private val skullTextureCache = mutableMapOf, Any>() + private val sentinelPresentInvalid = Object() + + val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex() + fun getSkullId(profile: GameProfile): String? { + val textures = profile.properties.get(PlayerSkinProvider.TEXTURES) + val textureProperty = textures.singleOrNull() ?: return null + val texture = decodeProfileTextureProperty(textureProperty) ?: return null + val textureUrl = + texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null + val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null + return mcUrlData.groupValues[1] + } + + fun getSkullTexture(profile: GameProfile): Identifier? { + val id = getSkullId(profile) ?: return null + return Identifier("firmskyblock", "textures/placedskull/$id.png") + } + + fun modifySkullTexture( + type: SkullBlock.SkullType?, + profile: GameProfile?, + cir: CallbackInfoReturnable + ) { + if (type != SkullBlock.Type.PLAYER) return + if (!TConfig.skullsEnabled) return + if (profile == null) return + val ic = IdentityCharacteristics(profile) + + val n = skullTextureCache.getOrPut(ic) { + val id = getSkullTexture(profile) ?: return@getOrPut sentinelPresentInvalid + if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { + return@getOrPut sentinelPresentInvalid + } + return@getOrPut id + } + if (n === sentinelPresentInvalid) return + cir.returnValue = RenderLayer.getEntityTranslucent(n as Identifier) + } } diff --git a/src/main/kotlin/moe/nea/firmament/util/IdentityCharacteristics.kt b/src/main/kotlin/moe/nea/firmament/util/IdentityCharacteristics.kt new file mode 100644 index 0000000..e6dad79 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/util/IdentityCharacteristics.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.util + +class IdentityCharacteristics(val value: T) { + override fun equals(other: Any?): Boolean { + return value === other + } + + override fun hashCode(): Int { + return System.identityHashCode(value) + } +} diff --git a/src/main/kotlin/moe/nea/firmament/util/MC.kt b/src/main/kotlin/moe/nea/firmament/util/MC.kt index 78f2eec..0c09306 100644 --- a/src/main/kotlin/moe/nea/firmament/util/MC.kt +++ b/src/main/kotlin/moe/nea/firmament/util/MC.kt @@ -7,21 +7,44 @@ package moe.nea.firmament.util import io.github.moulberry.repo.data.Coordinate +import java.util.concurrent.ConcurrentLinkedQueue import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.text.Text import net.minecraft.util.math.BlockPos +import moe.nea.firmament.events.TickEvent object MC { + + private val messageQueue = ConcurrentLinkedQueue() + + init { + TickEvent.subscribe { + while (true) { + inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) + } + } + } + + fun sendChat(text: Text) { + if (instance.isOnThread) + inGameHud.chatHud.addMessage(text) + else + messageQueue.add(text) + } + fun sendCommand(command: String) { player?.networkHandler?.sendCommand(command) } + inline val instance get() = MinecraftClient.getInstance() inline val keyboard get() = MinecraftClient.getInstance().keyboard inline val textureManager get() = MinecraftClient.getInstance().textureManager inline val inGameHud get() = MinecraftClient.getInstance().inGameHud inline val font get() = MinecraftClient.getInstance().textRenderer inline val soundManager get() = MinecraftClient.getInstance().soundManager inline val player get() = MinecraftClient.getInstance().player + inline val camera get() = MinecraftClient.getInstance().cameraEntity inline val world get() = MinecraftClient.getInstance().world inline var screen get() = MinecraftClient.getInstance().currentScreen diff --git a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt index 5b440b1..4d4d386 100644 --- a/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt +++ b/src/main/kotlin/moe/nea/firmament/util/item/SkullItemData.kt @@ -11,14 +11,16 @@ package moe.nea.firmament.util.item import com.mojang.authlib.GameProfile import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.properties.Property -import java.util.UUID +import java.util.* import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers +import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import net.minecraft.client.texture.PlayerSkinProvider import moe.nea.firmament.Firmament +import moe.nea.firmament.util.assertTrueOr import moe.nea.firmament.util.json.DashlessUUIDSerializer import moe.nea.firmament.util.json.InstantAsLongSerializer @@ -30,9 +32,9 @@ data class MinecraftProfileTextureKt( @Serializable data class MinecraftTexturesPayloadKt( - val textures: Map, - val profileId: UUID, - val profileName: String, + val textures: Map = mapOf(), + val profileId: UUID? = null, + val profileName: String? = null, val isPublic: Boolean = true, val timestamp: Instant = Clock.System.now(), ) @@ -43,3 +45,16 @@ fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) { properties.put(PlayerSkinProvider.TEXTURES, Property(PlayerSkinProvider.TEXTURES, encoded)) } +fun decodeProfileTextureProperty(property: Property): MinecraftTexturesPayloadKt? { + assertTrueOr(property.name == PlayerSkinProvider.TEXTURES) { return null } + try { + val json = java.util.Base64.getDecoder().decode(property.value).decodeToString() + return Firmament.json.decodeFromString(json) + } catch (e: Exception) { + // Malformed profile data + if (Firmament.DEBUG) + e.printStackTrace() + return null + } +} + diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json index 72d9830..396e365 100644 --- a/src/main/resources/assets/firmament/lang/en_us.json +++ b/src/main/resources/assets/firmament/lang/en_us.json @@ -107,11 +107,13 @@ "firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures", "firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration", "firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures", + "firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures", "firmament.config.fixes": "Fixes", "firmament.config.fixes.player-skins": "Fix unsigned Player Skins", "firmament.config.power-user.show-item-id": "Show SkyBlock Ids", "firmament.config.power-user.copy-item-id": "Copy SkyBlock Id", "firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id", + "firmament.config.power-user.copy-skull-texture": "Copy Placed Skull Id", "firmament.config.power-user.copy-nbt-data": "Copy NBT data", "firmament.config.power-user": "Power Users", "firmament.tooltip.skyblockid": "SkyBlock Id: %s", @@ -119,5 +121,7 @@ "firmament.tooltip.copied.skyblockid": "Copied SkyBlock Id: %s", "firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id", "firmament.tooltip.copied.modelid": "Copied Texture Id: %s", + "firmament.tooltip.copied.skull": "Copied Skull Id: %s", + "firmament.tooltip.copied.skull.fail": "Failed to copy skull id.", "firmament.tooltip.copied.nbt": "Copied NBT data" } -- cgit