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)
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:<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,
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/<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 {
prettyPrint = DEBUG
isLenient = true
ignoreUnknownKeys = true
encodeDefaults = true
}

View File

@@ -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

View File

@@ -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<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
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<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) {
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

View File

@@ -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<MinecraftProfileTexture.Type, MinecraftProfileTextureKt>,
val profileId: UUID,
val profileName: String,
val textures: Map<MinecraftProfileTexture.Type, MinecraftProfileTextureKt> = 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<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.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"
}