Add custom textures to placed skulls

This commit is contained in:
nea
2023-09-09 04:50:29 +02:00
parent dd974fcb79
commit c82c051704
9 changed files with 184 additions and 7 deletions

View File

@@ -8,11 +8,19 @@ SPDX-License-Identifier: CC0-1.0
## Items by internal id (ExtraAttributes) ## 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 Find the internal id of the item. This is usually stored in the ExtraAttributes tag (Check the Power User Config for
finding the texture pack id specifically). Once you found it, create an item model in a resource pack like you would 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:<internalid>`. So for an aspect of the end, this would be a vanilla item model, but at the coordinate `firmskyblock:<internalid>`. 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, `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 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. 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/<thathash>.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.

View File

@@ -0,0 +1,25 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* 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<RenderLayer> cir) {
CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir);
}
}

View File

@@ -60,6 +60,7 @@ object Firmament {
val json = Json { val json = Json {
prettyPrint = DEBUG prettyPrint = DEBUG
isLenient = true
ignoreUnknownKeys = true ignoreUnknownKeys = true
encodeDefaults = true encodeDefaults = true
} }

View File

@@ -6,17 +6,24 @@
package moe.nea.firmament.features.debug 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.item.ItemStack
import net.minecraft.text.Text 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.CustomItemModelEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.events.ScreenOpenEvent import moe.nea.firmament.events.ScreenOpenEvent
import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.skyBlockId import moe.nea.firmament.util.skyBlockId
object PowerUserTools : FirmamentFeature { object PowerUserTools : FirmamentFeature {
@@ -28,6 +35,7 @@ object PowerUserTools : FirmamentFeature {
val copyItemId by keyBindingWithDefaultUnbound("copy-item-id") val copyItemId by keyBindingWithDefaultUnbound("copy-item-id")
val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id") val copyTexturePackId by keyBindingWithDefaultUnbound("copy-texture-pack-id")
val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data") val copyNbtData by keyBindingWithDefaultUnbound("copy-nbt-data")
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
} }
override val config override val config
@@ -55,6 +63,29 @@ object PowerUserTools : FirmamentFeature {
lastCopiedStackViewTime = true lastCopiedStackViewTime = true
it.lines.add(text) 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 { TickEvent.subscribe {
if (!lastCopiedStackViewTime) if (!lastCopiedStackViewTime)
lastCopiedStack = null lastCopiedStack = null

View File

@@ -6,11 +6,21 @@
package moe.nea.firmament.features.texturepack 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.client.util.ModelIdentifier
import net.minecraft.util.Identifier
import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.TickEvent import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig 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 import moe.nea.firmament.util.skyBlockId
object CustomSkyBlockTextures : FirmamentFeature { object CustomSkyBlockTextures : FirmamentFeature {
@@ -19,6 +29,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
object TConfig : ManagedConfig(identifier) { object TConfig : ManagedConfig(identifier) {
val enabled by toggle("enabled") { true } val enabled by toggle("enabled") { true }
val skullsEnabled by toggle("skulls-enabled") { true }
val cacheDuration by integer("cache-duration", 0, 20) { 1 } val cacheDuration by integer("cache-duration", 0, 20) { 1 }
} }
@@ -32,8 +43,50 @@ object CustomSkyBlockTextures : FirmamentFeature {
it.overrideModel = ModelIdentifier("firmskyblock", id.identifier.path, "inventory") it.overrideModel = ModelIdentifier("firmskyblock", id.identifier.path, "inventory")
} }
TickEvent.subscribe { TickEvent.subscribe {
if (it.tickCount % TConfig.cacheDuration == 0) if (it.tickCount % TConfig.cacheDuration == 0) {
CustomItemModelEvent.clearCache() CustomItemModelEvent.clearCache()
skullTextureCache.clear()
}
} }
} }
private val skullTextureCache = mutableMapOf<IdentityCharacteristics<GameProfile>, 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<RenderLayer>
) {
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)
}
} }

View File

@@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package moe.nea.firmament.util
class IdentityCharacteristics<T>(val value: T) {
override fun equals(other: Any?): Boolean {
return value === other
}
override fun hashCode(): Int {
return System.identityHashCode(value)
}
}

View File

@@ -7,21 +7,44 @@
package moe.nea.firmament.util package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate import io.github.moulberry.repo.data.Coordinate
import java.util.concurrent.ConcurrentLinkedQueue
import net.minecraft.client.MinecraftClient import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.ingame.HandledScreen import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.text.Text
import net.minecraft.util.math.BlockPos import net.minecraft.util.math.BlockPos
import moe.nea.firmament.events.TickEvent
object MC { object MC {
private val messageQueue = ConcurrentLinkedQueue<Text>()
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) { fun sendCommand(command: String) {
player?.networkHandler?.sendCommand(command) player?.networkHandler?.sendCommand(command)
} }
inline val instance get() = MinecraftClient.getInstance()
inline val keyboard get() = MinecraftClient.getInstance().keyboard inline val keyboard get() = MinecraftClient.getInstance().keyboard
inline val textureManager get() = MinecraftClient.getInstance().textureManager inline val textureManager get() = MinecraftClient.getInstance().textureManager
inline val inGameHud get() = MinecraftClient.getInstance().inGameHud inline val inGameHud get() = MinecraftClient.getInstance().inGameHud
inline val font get() = MinecraftClient.getInstance().textRenderer inline val font get() = MinecraftClient.getInstance().textRenderer
inline val soundManager get() = MinecraftClient.getInstance().soundManager inline val soundManager get() = MinecraftClient.getInstance().soundManager
inline val player get() = MinecraftClient.getInstance().player inline val player get() = MinecraftClient.getInstance().player
inline val camera get() = MinecraftClient.getInstance().cameraEntity
inline val world get() = MinecraftClient.getInstance().world inline val world get() = MinecraftClient.getInstance().world
inline var screen inline var screen
get() = MinecraftClient.getInstance().currentScreen get() = MinecraftClient.getInstance().currentScreen

View File

@@ -11,14 +11,16 @@ package moe.nea.firmament.util.item
import com.mojang.authlib.GameProfile import com.mojang.authlib.GameProfile
import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property import com.mojang.authlib.properties.Property
import java.util.UUID import java.util.*
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.Instant import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers import kotlinx.serialization.UseSerializers
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import net.minecraft.client.texture.PlayerSkinProvider import net.minecraft.client.texture.PlayerSkinProvider
import moe.nea.firmament.Firmament import moe.nea.firmament.Firmament
import moe.nea.firmament.util.assertTrueOr
import moe.nea.firmament.util.json.DashlessUUIDSerializer import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.json.InstantAsLongSerializer import moe.nea.firmament.util.json.InstantAsLongSerializer
@@ -30,9 +32,9 @@ data class MinecraftProfileTextureKt(
@Serializable @Serializable
data class MinecraftTexturesPayloadKt( data class MinecraftTexturesPayloadKt(
val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt>, val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt> = mapOf(),
val profileId: UUID, val profileId: UUID? = null,
val profileName: String, val profileName: String? = null,
val isPublic: Boolean = true, val isPublic: Boolean = true,
val timestamp: Instant = Clock.System.now(), val timestamp: Instant = Clock.System.now(),
) )
@@ -43,3 +45,16 @@ fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) {
properties.put(PlayerSkinProvider.TEXTURES, Property(PlayerSkinProvider.TEXTURES, encoded)) 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<MinecraftTexturesPayloadKt>(json)
} catch (e: Exception) {
// Malformed profile data
if (Firmament.DEBUG)
e.printStackTrace()
return null
}
}

View File

@@ -107,11 +107,13 @@
"firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures", "firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures",
"firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration", "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.enabled": "Enable Custom Item Textures",
"firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures",
"firmament.config.fixes": "Fixes", "firmament.config.fixes": "Fixes",
"firmament.config.fixes.player-skins": "Fix unsigned Player Skins", "firmament.config.fixes.player-skins": "Fix unsigned Player Skins",
"firmament.config.power-user.show-item-id": "Show SkyBlock Ids", "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-item-id": "Copy SkyBlock Id",
"firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack 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.copy-nbt-data": "Copy NBT data",
"firmament.config.power-user": "Power Users", "firmament.config.power-user": "Power Users",
"firmament.tooltip.skyblockid": "SkyBlock Id: %s", "firmament.tooltip.skyblockid": "SkyBlock Id: %s",
@@ -119,5 +121,7 @@
"firmament.tooltip.copied.skyblockid": "Copied SkyBlock Id: %s", "firmament.tooltip.copied.skyblockid": "Copied SkyBlock Id: %s",
"firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id", "firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id",
"firmament.tooltip.copied.modelid": "Copied Texture Id: %s", "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" "firmament.tooltip.copied.nbt": "Copied NBT data"
} }