aboutsummaryrefslogtreecommitdiff
path: root/src/texturePacks/java/moe/nea
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-12-07 13:26:03 +0100
committerLinnea Gräf <nea@nea.moe>2024-12-07 13:26:03 +0100
commitbf7795df22ca7892fae1238403feebb57c005562 (patch)
tree3a49db5bade38498f976321b6e9008203f2d4d3b /src/texturePacks/java/moe/nea
parentcdb5e60f52eea9030af9ca2d4a49913664023965 (diff)
downloadFirmament-bf7795df22ca7892fae1238403feebb57c005562.tar.gz
Firmament-bf7795df22ca7892fae1238403feebb57c005562.tar.bz2
Firmament-bf7795df22ca7892fae1238403feebb57c005562.zip
WIP: Port to compilation on 1.21.4
Diffstat (limited to 'src/texturePacks/java/moe/nea')
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt301
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt171
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt154
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt103
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt117
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt66
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt8
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt8
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt23
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt85
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt69
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt159
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt75
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt19
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt28
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt22
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt271
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt34
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt22
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt21
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt124
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt29
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt66
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java11
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java26
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java30
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java23
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java31
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java27
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java30
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java38
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java21
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java24
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java48
34 files changed, 2284 insertions, 0 deletions
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt
new file mode 100644
index 0000000..e7c379b
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt
@@ -0,0 +1,301 @@
+@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class)
+
+package moe.nea.firmament.features.texturepack
+
+import java.util.concurrent.CompletableFuture
+import net.fabricmc.loader.api.FabricLoader
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.serializer
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.client.render.model.BakedModel
+import net.minecraft.client.util.ModelIdentifier
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SinglePreparationResourceReloader
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.profiler.Profiler
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.BakeExtraModelsEvent
+import moe.nea.firmament.events.EarlyResourceReloadEvent
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
+import moe.nea.firmament.util.IdentifierSerializer
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.json.BlockPosSerializer
+import moe.nea.firmament.util.json.SingletonSerializableList
+
+
+object CustomBlockTextures {
+ @Serializable
+ data class CustomBlockOverride(
+ val modes: @Serializable(SingletonSerializableList::class) List<String>,
+ val area: List<Area>? = null,
+ val replacements: Map<Identifier, Replacement>,
+ )
+
+ @Serializable(with = Replacement.Serializer::class)
+ data class Replacement(
+ val block: Identifier,
+ val sound: Identifier?,
+ ) {
+
+ @Transient
+ val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament")
+
+ @Transient
+ val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) {
+ MC.instance.bakedModelManager.getModel(blockModelIdentifier)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ @kotlinx.serialization.Serializer(Replacement::class)
+ object DefaultSerializer : KSerializer<Replacement>
+
+ object Serializer : KSerializer<Replacement> {
+ val delegate = serializer<JsonElement>()
+ override val descriptor: SerialDescriptor
+ get() = delegate.descriptor
+
+ override fun deserialize(decoder: Decoder): Replacement {
+ val jsonElement = decoder.decodeSerializableValue(delegate)
+ if (jsonElement is JsonPrimitive) {
+ require(jsonElement.isString)
+ return Replacement(Identifier.tryParse(jsonElement.content)!!, null)
+ }
+ return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement)
+ }
+
+ override fun serialize(encoder: Encoder, value: Replacement) {
+ encoder.encodeSerializableValue(DefaultSerializer, value)
+ }
+ }
+ }
+
+ @Serializable
+ data class Area(
+ val min: BlockPos,
+ val max: BlockPos,
+ ) {
+ @Transient
+ val realMin = BlockPos(
+ minOf(min.x, max.x),
+ minOf(min.y, max.y),
+ minOf(min.z, max.z),
+ )
+
+ @Transient
+ val realMax = BlockPos(
+ maxOf(min.x, max.x),
+ maxOf(min.y, max.y),
+ maxOf(min.z, max.z),
+ )
+
+ fun roughJoin(other: Area): Area {
+ return Area(
+ BlockPos(
+ minOf(realMin.x, other.realMin.x),
+ minOf(realMin.y, other.realMin.y),
+ minOf(realMin.z, other.realMin.z),
+ ),
+ BlockPos(
+ maxOf(realMax.x, other.realMax.x),
+ maxOf(realMax.y, other.realMax.y),
+ maxOf(realMax.z, other.realMax.z),
+ )
+ )
+ }
+
+ fun contains(blockPos: BlockPos): Boolean {
+ return (blockPos.x in realMin.x..realMax.x) &&
+ (blockPos.y in realMin.y..realMax.y) &&
+ (blockPos.z in realMin.z..realMax.z)
+ }
+ }
+
+ data class LocationReplacements(
+ val lookup: Map<Block, List<BlockReplacement>>
+ )
+
+ data class BlockReplacement(
+ val checks: List<Area>?,
+ val replacement: Replacement,
+ ) {
+ val roughCheck by lazy(LazyThreadSafetyMode.NONE) {
+ if (checks == null || checks.size < 3) return@lazy null
+ checks.reduce { acc, next -> acc.roughJoin(next) }
+ }
+ }
+
+ data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>)
+
+ var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf())
+ var currentIslandReplacements: LocationReplacements? = null
+
+ fun refreshReplacements() {
+ val location = SBData.skyblockLocation
+ val replacements =
+ if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get)
+ else null
+ val lastReplacements = currentIslandReplacements
+ currentIslandReplacements = replacements
+ if (lastReplacements != replacements) {
+ MC.nextTick {
+ MC.worldRenderer.chunks?.chunks?.forEach {
+ // false schedules rebuilds outside a 27 block radius to happen async
+ it.scheduleRebuild(false)
+ }
+ sodiumReloadTask?.run()
+ }
+ }
+ }
+
+ private val sodiumReloadTask = runCatching {
+ val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader")
+ .getConstructor()
+ .newInstance() as Runnable
+ r.run()
+ r
+ }.getOrElse {
+ if (FabricLoader.getInstance().isModLoaded("sodium"))
+ logger.error("Could not create sodium chunk reloader")
+ null
+ }
+
+
+ fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean {
+ if (blockPos == null) return true
+ val rc = replacement.roughCheck
+ if (rc != null && !rc.contains(blockPos)) return false
+ val areas = replacement.checks
+ if (areas != null && !areas.any { it.contains(blockPos) }) return false
+ return true
+ }
+
+ @JvmStatic
+ fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BakedModel? {
+ return getReplacement(block, blockPos)?.bakedModel
+ }
+
+ @JvmStatic
+ fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? {
+ if (isInFallback() && blockPos == null) {
+ return null
+ }
+ val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null
+ for (replacement in replacements) {
+ if (replacement.checks == null || matchesPosition(replacement, blockPos))
+ return replacement.replacement
+ }
+ return null
+ }
+
+
+ @Subscribe
+ fun onLocation(event: SkyblockServerUpdateEvent) {
+ refreshReplacements()
+ }
+
+ @Volatile
+ var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
+ mapOf()))
+
+ val insideFallbackCall = ThreadLocal.withInitial { 0 }
+
+ @JvmStatic
+ fun enterFallbackCall() {
+ insideFallbackCall.set(insideFallbackCall.get() + 1)
+ }
+
+ fun isInFallback() = insideFallbackCall.get() > 0
+
+ @JvmStatic
+ fun exitFallbackCall() {
+ insideFallbackCall.set(insideFallbackCall.get() - 1)
+ }
+
+ @Subscribe
+ fun onEarlyReload(event: EarlyResourceReloadEvent) {
+ preparationFuture = CompletableFuture
+ .supplyAsync(
+ { prepare(event.resourceManager) }, event.preparationExecutor)
+ }
+
+ @Subscribe
+ fun bakeExtraModels(event: BakeExtraModelsEvent) {
+ preparationFuture.join().data.values
+ .flatMap { it.lookup.values }
+ .flatten()
+ .mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier }
+ .forEach { event.addNonItemModel(it, it.id) }
+ }
+
+ private fun prepare(manager: ResourceManager): BakedReplacements {
+ val resources = manager.findResources("overrides/blocks") {
+ it.namespace == "firmskyblock" && it.path.endsWith(".json")
+ }
+ val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>()
+ for ((file, resource) in resources) {
+ val json =
+ Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream)
+ .getOrElse { ex ->
+ logger.error("Failed to load block texture override at $file", ex)
+ continue
+ }
+ for (mode in json.modes) {
+ val island = SkyBlockIsland.forMode(mode)
+ val islandMpa = map.getOrPut(island, ::mutableMapOf)
+ for ((blockId, replacement) in json.replacements) {
+ val block = MC.defaultRegistries.getOrThrow(RegistryKeys.BLOCK)
+ .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId))
+ .getOrNull()
+ if (block == null) {
+ logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'")
+ continue
+ }
+ val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf)
+ replacements.add(BlockReplacement(json.area, replacement))
+ }
+ }
+ }
+
+ return BakedReplacements(map.mapValues { LocationReplacements(it.value) })
+ }
+
+ @JvmStatic
+ fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel {
+ return getReplacementModel(state, pos) ?: orig
+ }
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ event.resourceManager.registerReloader(object :
+ SinglePreparationResourceReloader<BakedReplacements>() {
+ override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
+ return preparationFuture.join()
+ }
+
+ override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) {
+ allLocationReplacements = prepared
+ refreshReplacements()
+ }
+ })
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt
new file mode 100644
index 0000000..84c04af
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt
@@ -0,0 +1,171 @@
+@file:UseSerializers(IdentifierSerializer::class)
+
+package moe.nea.firmament.features.texturepack
+
+import java.util.Optional
+import java.util.concurrent.atomic.AtomicInteger
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.UseSerializers
+import net.minecraft.client.render.entity.equipment.EquipmentModel
+import net.minecraft.component.type.EquippableComponent
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.item.ItemStack
+import net.minecraft.item.equipment.EquipmentAssetKeys
+import net.minecraft.registry.RegistryKey
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SinglePreparationResourceReloader
+import net.minecraft.util.Identifier
+import net.minecraft.util.profiler.Profiler
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
+import moe.nea.firmament.util.IdentifierSerializer
+import moe.nea.firmament.util.collections.WeakCache
+import moe.nea.firmament.util.intoOptional
+import moe.nea.firmament.util.skyBlockId
+
+object CustomGlobalArmorOverrides {
+ @Serializable
+ data class ArmorOverride(
+ @SerialName("item_ids")
+ val itemIds: List<String>,
+ val layers: List<ArmorOverrideLayer>? = null,
+ val model: Identifier? = null,
+ val overrides: List<ArmorOverrideOverride> = listOf(),
+ ) {
+ @Transient
+ lateinit var modelIdentifier: Identifier
+ fun bake(manager: ResourceManager) {
+ modelIdentifier = bakeModel(model, layers)
+ overrides.forEach { it.bake(manager) }
+ }
+
+ init {
+ require(layers != null || model != null) { "Either model or layers must be specified for armor override" }
+ require(layers == null || model == null) { "Can't specify both model and layers for armor override" }
+ }
+ }
+
+ @Serializable
+ data class ArmorOverrideLayer(
+ val tint: Boolean = false,
+ val identifier: Identifier,
+ val suffix: String = "",
+ )
+
+ @Serializable
+ data class ArmorOverrideOverride(
+ val predicate: FirmamentModelPredicate,
+ val layers: List<ArmorOverrideLayer>? = null,
+ val model: Identifier? = null,
+ ) {
+ init {
+ require(layers != null || model != null) { "Either model or layers must be specified for armor override override" }
+ require(layers == null || model == null) { "Can't specify both model and layers for armor override override" }
+ }
+
+ @Transient
+ lateinit var modelIdentifier: Identifier
+ fun bake(manager: ResourceManager) {
+ modelIdentifier = bakeModel(model, layers)
+ }
+ }
+
+
+ private fun resolveComponent(slot: EquipmentSlot, model: Identifier): EquippableComponent {
+ return EquippableComponent(
+ slot,
+ null,
+ Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)),
+ Optional.empty(),
+ Optional.empty(), false, false, false
+ )
+ }
+
+ val overrideCache =
+ WeakCache.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot ->
+ val id = stack.skyBlockId ?: return@memoize Optional.empty()
+ val override = overrides[id.neuItem] ?: return@memoize Optional.empty()
+ for (suboverride in override.overrides) {
+ if (suboverride.predicate.test(stack)) {
+ return@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional()
+ }
+ }
+ return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional()
+ }
+
+ var overrides: Map<String, ArmorOverride> = mapOf()
+ private var bakedOverrides: MutableMap<Identifier, EquipmentModel> = mutableMapOf()
+ private val sentinelFirmRunning = AtomicInteger()
+
+ private fun bakeModel(model: Identifier?, layers: List<ArmorOverrideLayer>?): Identifier {
+ require(model == null || layers == null)
+ if (model != null) {
+ return model
+ } else if (layers != null) {
+ val idNumber = sentinelFirmRunning.incrementAndGet()
+ val identifier = Identifier.of("firmament:sentinel/$idNumber")
+ val equipmentLayers = layers.map {
+ EquipmentModel.Layer(
+ it.identifier, if (it.tint) {
+ Optional.of(EquipmentModel.Dyeable(Optional.empty()))
+ } else {
+ Optional.empty()
+ },
+ false
+ )
+ }
+ bakedOverrides[identifier] = EquipmentModel(
+ mapOf(
+ EquipmentModel.LayerType.HUMANOID to equipmentLayers,
+ EquipmentModel.LayerType.HUMANOID_LEGGINGS to equipmentLayers,
+ )
+ )
+ return identifier
+ } else {
+ error("Either model or layers must be non null")
+ }
+ }
+
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ event.resourceManager.registerReloader(object :
+ SinglePreparationResourceReloader<Map<String, ArmorOverride>>() {
+ override fun prepare(manager: ResourceManager, profiler: Profiler): Map<String, ArmorOverride> {
+ val overrideFiles = manager.findResources("overrides/armor_models") {
+ it.namespace == "firmskyblock" && it.path.endsWith(".json")
+ }
+ val overrides = overrideFiles.mapNotNull {
+ Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex ->
+ logger.error("Failed to load armor texture override at ${it.key}", ex)
+ null
+ }
+ }
+ val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } }
+ .toMap()
+ associatedMap.forEach { it.value.bake(manager) }
+ return associatedMap
+ }
+
+ override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
+ bakedOverrides.clear()
+ overrides = prepared
+ }
+ })
+ }
+
+ @JvmStatic
+ fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<EquippableComponent> {
+ return overrideCache.invoke(itemStack, slot)
+ }
+
+ @JvmStatic
+ fun overrideArmorLayer(id: Identifier): EquipmentModel? {
+ return bakedOverrides[id]
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt
new file mode 100644
index 0000000..ad44b03
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt
@@ -0,0 +1,154 @@
+@file:UseSerializers(IdentifierSerializer::class, FirmamentRootPredicateSerializer::class)
+
+package moe.nea.firmament.features.texturepack
+
+
+import java.util.concurrent.CompletableFuture
+import org.slf4j.LoggerFactory
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.client.util.ModelIdentifier
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SinglePreparationResourceReloader
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.profiler.Profiler
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.BakeExtraModelsEvent
+import moe.nea.firmament.events.CustomItemModelEvent
+import moe.nea.firmament.events.EarlyResourceReloadEvent
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.events.ScreenChangeEvent
+import moe.nea.firmament.events.subscription.SubscriptionOwner
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.IdentifierSerializer
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.json.SingletonSerializableList
+import moe.nea.firmament.util.runNull
+
+object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalTextures.CustomGuiTextureOverride>(),
+ SubscriptionOwner {
+ override val delegateFeature: FirmamentFeature
+ get() = CustomSkyBlockTextures
+
+ class CustomGuiTextureOverride(
+ val classes: List<ItemOverrideCollection>
+ )
+
+ @Serializable
+ data class GlobalItemOverride(
+ val screen: @Serializable(SingletonSerializableList::class) List<Identifier>,
+ val model: Identifier,
+ val predicate: FirmamentModelPredicate,
+ )
+
+ @Serializable
+ data class ScreenFilter(
+ val title: StringMatcher,
+ )
+
+ data class ItemOverrideCollection(
+ val screenFilter: ScreenFilter,
+ val overrides: List<GlobalItemOverride>,
+ )
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ MC.resourceManager.registerReloader(this)
+ }
+
+ @Subscribe
+ fun onEarlyReload(event: EarlyResourceReloadEvent) {
+ preparationFuture = CompletableFuture
+ .supplyAsync(
+ {
+ prepare(event.resourceManager)
+ }, event.preparationExecutor)
+ }
+
+ @Subscribe
+ fun onBakeModels(event: BakeExtraModelsEvent) {
+ for (guiClassOverride in preparationFuture.join().classes) {
+ for (override in guiClassOverride.overrides) {
+ event.addItemModel(ModelIdentifier(override.model, "inventory"))
+ }
+ }
+ }
+
+ @Volatile
+ var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture(
+ CustomGuiTextureOverride(listOf()))
+
+ override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride {
+ return preparationFuture.join()
+ }
+
+ override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) {
+ guiClassOverrides = prepared
+ }
+
+ val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java)
+ fun prepare(manager: ResourceManager): CustomGuiTextureOverride {
+ val overrideResources =
+ manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") }
+ .mapNotNull {
+ Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex ->
+ ErrorUtil.softError("Failed to load global item override at ${it.key}", ex)
+ null
+ }
+ }
+
+ val byGuiClass = overrideResources.flatMap { override -> override.screen.toSet().map { it to override } }
+ .groupBy { it.first }
+ val guiClasses = byGuiClass.entries
+ .mapNotNull {
+ val key = it.key
+ val guiClassResource =
+ manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json"))
+ .getOrNull()
+ ?: return@mapNotNull runNull {
+ ErrorUtil.softError("Failed to locate screen filter at $key")
+ }
+ val screenFilter =
+ Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
+ .getOrElse { ex ->
+ ErrorUtil.softError("Failed to load screen filter at $key", ex)
+ return@mapNotNull null
+ }
+ ItemOverrideCollection(screenFilter, it.value.map { it.second })
+ }
+ logger.info("Loaded ${overrideResources.size} global item overrides")
+ return CustomGuiTextureOverride(guiClasses)
+ }
+
+ var guiClassOverrides = CustomGuiTextureOverride(listOf())
+
+ var matchingOverrides: Set<ItemOverrideCollection> = setOf()
+
+ @Subscribe
+ fun onOpenGui(event: ScreenChangeEvent) {
+ val newTitle = event.new?.title ?: Text.empty()
+ matchingOverrides = guiClassOverrides.classes
+ .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) }
+ }
+
+ @Subscribe
+ fun replaceGlobalModel(event: CustomItemModelEvent) {
+ val override = matchingOverrides
+ .firstNotNullOfOrNull {
+ it.overrides
+ .asSequence()
+ .filter { it.predicate.test(event.itemStack) }
+ .map { it.model }
+ .firstOrNull()
+ }
+
+ if (override != null)
+ event.overrideIfExists(override)
+ }
+
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
new file mode 100644
index 0000000..6472993
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonObject
+import com.mojang.datafixers.util.Pair
+import com.mojang.serialization.Codec
+import com.mojang.serialization.DataResult
+import com.mojang.serialization.Decoder
+import com.mojang.serialization.DynamicOps
+import com.mojang.serialization.Encoder
+import net.minecraft.client.render.item.model.ItemModelTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.features.texturepack.predicates.AndPredicate
+import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate
+import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate
+import moe.nea.firmament.features.texturepack.predicates.ItemPredicate
+import moe.nea.firmament.features.texturepack.predicates.LorePredicate
+import moe.nea.firmament.features.texturepack.predicates.NotPredicate
+import moe.nea.firmament.features.texturepack.predicates.OrPredicate
+import moe.nea.firmament.features.texturepack.predicates.PetPredicate
+import moe.nea.firmament.util.json.KJsonOps
+
+object CustomModelOverrideParser {
+
+ val LEGACY_CODEC: Codec<FirmamentModelPredicate> =
+ Codec.of(
+ Encoder.error("cannot encode legacy firmament model predicates"),
+ object : Decoder<FirmamentModelPredicate> {
+ override fun <T : Any?> decode(
+ ops: DynamicOps<T>,
+ input: T
+ ): DataResult<Pair<FirmamentModelPredicate, T>> {
+ try {
+ val pred = Firmament.json.decodeFromJsonElement(
+ FirmamentRootPredicateSerializer,
+ ops.convertTo(KJsonOps.INSTANCE, input))
+ return DataResult.success(Pair.of(pred, ops.empty()))
+ } catch (ex: Exception) {
+ return DataResult.error { "Could not deserialize ${ex.message}" }
+ }
+ }
+ }
+ )
+
+ val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>()
+
+
+ fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) {
+ predicateParsers[Identifier.of("firmament", name)] = parser
+ }
+
+ init {
+ registerPredicateParser("display_name", DisplayNamePredicate.Parser)
+ registerPredicateParser("lore", LorePredicate.Parser)
+ registerPredicateParser("all", AndPredicate.Parser)
+ registerPredicateParser("any", OrPredicate.Parser)
+ registerPredicateParser("not", NotPredicate.Parser)
+ registerPredicateParser("item", ItemPredicate.Parser)
+ registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
+ registerPredicateParser("pet", PetPredicate.Parser)
+ }
+
+ private val neverPredicate = listOf(
+ object : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return false
+ }
+ }
+ )
+
+ fun parsePredicates(predicates: JsonObject): List<FirmamentModelPredicate> {
+ val parsedPredicates = mutableListOf<FirmamentModelPredicate>()
+ for (predicateName in predicates.keySet()) {
+ if (!predicateName.startsWith("firmament:")) continue
+ val identifier = Identifier.of(predicateName)
+ val parser = predicateParsers[identifier] ?: return neverPredicate
+ val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate
+ parsedPredicates.add(parsedPredicate)
+ }
+ return parsedPredicates
+ }
+
+ @JvmStatic
+ fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? {
+ val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null
+ val parsedPredicates = parsePredicates(predicates)
+ if (parsedPredicates.isEmpty())
+ return null
+ return parsedPredicates.toTypedArray()
+ }
+
+ @Subscribe
+ fun finalizeResources(event: FinalizeResourceManagerEvent) {
+ ItemModelTypes.ID_MAPPER.put(
+ Firmament.identifier("predicates/legacy"),
+ PredicateModel.Unbaked.CODEC
+ )
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
new file mode 100644
index 0000000..d9ca5b4
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
@@ -0,0 +1,117 @@
+package moe.nea.firmament.features.texturepack
+
+import com.mojang.authlib.minecraft.MinecraftProfileTexture
+import com.mojang.authlib.properties.Property
+import java.util.Optional
+import org.jetbrains.annotations.Nullable
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.block.SkullBlock
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.component.type.ProfileComponent
+import net.minecraft.util.Identifier
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.CustomItemModelEvent
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.collections.WeakCache
+import moe.nea.firmament.util.mc.decodeProfileTextureProperty
+import moe.nea.firmament.util.skyBlockId
+
+object CustomSkyBlockTextures : FirmamentFeature {
+ override val identifier: String
+ get() = "custom-skyblock-textures"
+
+ object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing?
+ val enabled by toggle("enabled") { true }
+ val skullsEnabled by toggle("skulls-enabled") { true }
+ val cacheForever by toggle("cache-forever") { true }
+ val cacheDuration by integer("cache-duration", 0, 100) { 1 }
+ val enableModelOverrides by toggle("model-overrides") { true }
+ val enableArmorOverrides by toggle("armor-overrides") { true }
+ val enableBlockOverrides by toggle("block-overrides") { true }
+ val enableLegacyCIT by toggle("legacy-cit") { true }
+ val allowRecoloringUiText by toggle("recolor-text") { true }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ val allItemCaches by lazy {
+ listOf(
+ skullTextureCache.cache,
+ CustomGlobalArmorOverrides.overrideCache.cache
+ )
+ }
+
+ init {
+ PowerUserTools.getSkullId = ::getSkullTexture
+ }
+
+ fun clearAllCaches() {
+ allItemCaches.forEach(WeakCache<*, *, *>::clear)
+ }
+
+ @Subscribe
+ fun onTick(it: TickEvent) {
+ if (TConfig.cacheForever) return
+ if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
+ clearAllCaches()
+ }
+ }
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ event.registerOnApply("Clear firmament CIT caches") {
+ clearAllCaches()
+ }
+ }
+
+ @Subscribe
+ fun onCustomModelId(it: CustomItemModelEvent) {
+ if (!TConfig.enabled) return
+ val id = it.itemStack.skyBlockId ?: return
+ it.overrideIfExists(Identifier.of("firmskyblock", id.identifier.path))
+ }
+
+ private val skullTextureCache =
+ WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component ->
+ val id = getSkullTexture(component) ?: return@memoize Optional.empty()
+ if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
+ return@memoize Optional.empty()
+ }
+ return@memoize Optional.of(id)
+ }
+
+ private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex()
+
+ fun getSkullId(textureProperty: Property): String? {
+ 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: ProfileComponent): Identifier? {
+ val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null
+ return Identifier.of("firmskyblock", "textures/placedskull/$id.png")
+ }
+
+ fun modifySkullTexture(
+ type: SkullBlock.SkullType?,
+ component: ProfileComponent?,
+ cir: CallbackInfoReturnable<RenderLayer>
+ ) {
+ if (type != SkullBlock.Type.PLAYER) return
+ if (!TConfig.skullsEnabled) return
+ if (component == null) return
+
+ val n = skullTextureCache.invoke(component).getOrNull() ?: return
+ cir.returnValue = RenderLayer.getEntityTranslucent(n)
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt
new file mode 100644
index 0000000..4ca1796
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt
@@ -0,0 +1,66 @@
+package moe.nea.firmament.features.texturepack
+
+import java.util.Optional
+import kotlinx.serialization.Serializable
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SinglePreparationResourceReloader
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.profiler.Profiler
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.FinalizeResourceManagerEvent
+import moe.nea.firmament.util.collections.WeakCache
+
+object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.TextOverrides?>() {
+ @Serializable
+ data class TextOverrides(
+ val defaultColor: Int,
+ val overrides: List<TextOverride> = listOf()
+ )
+
+ @Serializable
+ data class TextOverride(
+ val predicate: StringMatcher,
+ val override: Int,
+ )
+
+ @Subscribe
+ fun registerTextColorReloader(event: FinalizeResourceManagerEvent) {
+ event.resourceManager.registerReloader(this)
+ }
+
+ val cache = WeakCache.memoize<Text, Optional<Int>>("CustomTextColor") { text ->
+ val override = textOverrides ?: return@memoize Optional.empty()
+ Optional.of(override.overrides.find { it.predicate.matches(text) }?.override ?: override.defaultColor)
+ }
+
+ fun mapTextColor(text: Text, oldColor: Int): Int {
+ if (textOverrides == null) return oldColor
+ return cache(text).getOrNull() ?: oldColor
+ }
+
+ override fun prepare(
+ manager: ResourceManager,
+ profiler: Profiler
+ ): TextOverrides? {
+ val resource = manager.getResource(Identifier.of("firmskyblock", "overrides/text_colors.json")).getOrNull()
+ ?: return null
+ return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.inputStream)
+ .getOrElse {
+ Firmament.logger.error("Could not parse text_colors.json", it)
+ null
+ }
+ }
+
+ var textOverrides: TextOverrides? = null
+
+ override fun apply(
+ prepared: TextOverrides?,
+ manager: ResourceManager,
+ profiler: Profiler
+ ) {
+ textOverrides = prepared
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt
new file mode 100644
index 0000000..d11fec0
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt
@@ -0,0 +1,8 @@
+
+package moe.nea.firmament.features.texturepack
+
+import net.minecraft.item.ItemStack
+
+interface FirmamentModelPredicate {
+ fun test(stack: ItemStack): Boolean
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt
new file mode 100644
index 0000000..3ed0c67
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicateParser.kt
@@ -0,0 +1,8 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+
+interface FirmamentModelPredicateParser {
+ fun parse(jsonElement: JsonElement): FirmamentModelPredicate?
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt
new file mode 100644
index 0000000..0b8ae8e
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt
@@ -0,0 +1,23 @@
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonObject
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import moe.nea.firmament.features.texturepack.predicates.AndPredicate
+
+object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> {
+ val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer()
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor)
+
+ override fun deserialize(decoder: Decoder): FirmamentModelPredicate {
+ val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject
+ return AndPredicate(CustomModelOverrideParser.parsePredicates(json).toTypedArray())
+ }
+
+ override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) {
+ TODO("Cannot serialize firmament predicates")
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt
new file mode 100644
index 0000000..e21e69d
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt
@@ -0,0 +1,85 @@
+package moe.nea.firmament.features.texturepack
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.MapCodec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.client.item.ItemModelManager
+import net.minecraft.client.render.item.ItemRenderState
+import net.minecraft.client.render.item.model.ItemModel
+import net.minecraft.client.render.item.model.ItemModelTypes
+import net.minecraft.client.render.model.ResolvableModel
+import net.minecraft.client.world.ClientWorld
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.ModelTransformationMode
+
+class PredicateModel {
+ data class Baked(
+ val fallback: ItemModel,
+ val overrides: List<Override>
+ ) : ItemModel {
+ data class Override(
+ val model: ItemModel,
+ val predicate: FirmamentModelPredicate,
+ )
+
+ override fun update(
+ state: ItemRenderState,
+ stack: ItemStack,
+ resolver: ItemModelManager,
+ transformationMode: ModelTransformationMode,
+ world: ClientWorld?,
+ user: LivingEntity?,
+ seed: Int
+ ) {
+ val model =
+ overrides
+ .find { it.predicate.test(stack) }
+ ?.model
+ ?: fallback
+ model.update(state, stack, resolver, transformationMode, world, user, seed)
+ }
+ }
+
+ data class Unbaked(
+ val fallback: ItemModel.Unbaked,
+ val overrides: List<Override>,
+ ) : ItemModel.Unbaked {
+ companion object {
+ val OVERRIDE_CODEC: Codec<Override> = RecordCodecBuilder.create {
+ it.group(
+ ItemModelTypes.CODEC.fieldOf("model").forGetter(Override::model),
+ CustomModelOverrideParser.LEGACY_CODEC.fieldOf("predicate").forGetter(Override::predicate),
+ ).apply(it, Unbaked::Override)
+ }
+ val CODEC: MapCodec<Unbaked> =
+ RecordCodecBuilder.mapCodec {
+ it.group(
+ ItemModelTypes.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback),
+ OVERRIDE_CODEC.listOf().fieldOf("overrides").forGetter(Unbaked::overrides),
+ ).apply(it, ::Unbaked)
+ }
+ }
+
+ data class Override(
+ val model: ItemModel.Unbaked,
+ val predicate: FirmamentModelPredicate,
+ )
+
+ override fun resolve(resolver: ResolvableModel.Resolver) {
+ fallback.resolve(resolver)
+ overrides.forEach { it.model.resolve(resolver) }
+ }
+
+ override fun getCodec(): MapCodec<out Unbaked> {
+ return CODEC
+ }
+
+ override fun bake(context: ItemModel.BakeContext): ItemModel {
+ return Baked(
+ fallback.bake(context),
+ overrides.map { Baked.Override(it.model.bake(context), it.predicate) }
+ )
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt
new file mode 100644
index 0000000..634a171
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/RarityMatcher.kt
@@ -0,0 +1,69 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import io.github.moulberry.repo.data.Rarity
+import moe.nea.firmament.util.useMatch
+
+abstract class RarityMatcher {
+ abstract fun match(rarity: Rarity): Boolean
+
+ companion object {
+ fun parse(jsonElement: JsonElement): RarityMatcher {
+ val string = jsonElement.asString
+ val range = parseRange(string)
+ if (range != null) return range
+ return Exact(Rarity.valueOf(string))
+ }
+
+ private val allRarities = Rarity.entries.joinToString("|", "(?:", ")")
+ private val intervalSpec =
+ "(?<beginningOpen>[\\[\\(])(?<beginning>$allRarities)?,(?<ending>$allRarities)?(?<endingOpen>[\\]\\)])"
+ .toPattern()
+
+ fun parseRange(string: String): RangeMatcher? {
+ intervalSpec.useMatch<Nothing>(string) {
+ // Open in the set-theory sense, meaning does not include its end.
+ val beginningOpen = group("beginningOpen") == "("
+ val endingOpen = group("endingOpen") == ")"
+ val beginning = group("beginning")?.let(Rarity::valueOf)
+ val ending = group("ending")?.let(Rarity::valueOf)
+ return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
+ }
+ return null
+ }
+
+ }
+
+ data class Exact(val expected: Rarity) : RarityMatcher() {
+ override fun match(rarity: Rarity): Boolean {
+ return rarity == expected
+ }
+ }
+
+ data class RangeMatcher(
+ val beginning: Rarity?,
+ val beginningInclusive: Boolean,
+ val ending: Rarity?,
+ val endingInclusive: Boolean,
+ ) : RarityMatcher() {
+ override fun match(rarity: Rarity): Boolean {
+ if (beginning != null) {
+ if (beginningInclusive) {
+ if (rarity < beginning) return false
+ } else {
+ if (rarity <= beginning) return false
+ }
+ }
+ if (ending != null) {
+ if (endingInclusive) {
+ if (rarity > ending) return false
+ } else {
+ if (rarity >= ending) return false
+ }
+ }
+ return true
+ }
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt
new file mode 100644
index 0000000..2b13284
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt
@@ -0,0 +1,159 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import com.google.gson.internal.LazilyParsedNumber
+import java.util.function.Predicate
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import net.minecraft.nbt.NbtString
+import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.removeColorCodes
+
+@Serializable(with = StringMatcher.Serializer::class)
+interface StringMatcher {
+ fun matches(string: String): Boolean
+ fun matches(text: Text): Boolean {
+ return matches(text.string)
+ }
+
+ fun matches(nbt: NbtString): Boolean {
+ val string = nbt.asString()
+ val jsonStart = string.indexOf('{')
+ val stringStart = string.indexOf('"')
+ val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank()
+ val isJson = jsonStart >= 0 && string.subSequence(0, jsonStart).isBlank()
+ if (isString || isJson)
+ return matches(Text.Serialization.fromJson(string, MC.defaultRegistries) ?: return false)
+ return matches(string)
+ }
+
+ class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher {
+ private val expected = if (stripColorCodes) input.removeColorCodes() else input
+ override fun matches(string: String): Boolean {
+ return expected == (if (stripColorCodes) string.removeColorCodes() else string)
+ }
+
+ override fun toString(): String {
+ return "Equals($expected, stripColorCodes = $stripColorCodes)"
+ }
+ }
+
+ class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
+ private val regex: Predicate<String> = patternWithColorCodes.toPattern().asMatchPredicate()
+ override fun matches(string: String): Boolean {
+ return regex.test(if (stripColorCodes) string.removeColorCodes() else string)
+ }
+
+ override fun toString(): String {
+ return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)"
+ }
+ }
+
+ object Serializer : KSerializer<StringMatcher> {
+ val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer()
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor)
+
+ override fun deserialize(decoder: Decoder): StringMatcher {
+ val delegate = decoder.decodeSerializableValue(delegateSerializer)
+ val gsonDelegate = delegate.intoGson()
+ return parse(gsonDelegate)
+ }
+
+ override fun serialize(encoder: Encoder, value: StringMatcher) {
+ encoder.encodeSerializableValue(delegateSerializer, serialize(value).intoKotlinJson())
+ }
+
+ }
+
+ companion object {
+ fun serialize(stringMatcher: StringMatcher): JsonElement {
+ TODO("Cannot serialize string matchers rn")
+ }
+
+ fun parse(jsonElement: JsonElement): StringMatcher {
+ if (jsonElement is JsonPrimitive) {
+ return Equals(jsonElement.asString, true)
+ }
+ if (jsonElement is JsonObject) {
+ val regex = jsonElement["regex"] as JsonPrimitive?
+ val text = jsonElement["equals"] as JsonPrimitive?
+ val shouldStripColor = when (val color = (jsonElement["color"] as JsonPrimitive?)?.asString) {
+ "preserve" -> false
+ "strip", null -> true
+ else -> error("Unknown color preservation mode: $color")
+ }
+ if ((regex == null) == (text == null)) error("Could not parse $jsonElement as string matcher")
+ if (regex != null)
+ return Pattern(regex.asString, shouldStripColor)
+ if (text != null)
+ return Equals(text.asString, shouldStripColor)
+ }
+ error("Could not parse $jsonElement as a string matcher")
+ }
+ }
+}
+
+fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement {
+ when (this) {
+ is JsonNull -> return kotlinx.serialization.json.JsonNull
+ is JsonObject -> {
+ return kotlinx.serialization.json.JsonObject(this.entrySet()
+ .associate { it.key to it.value.intoKotlinJson() })
+ }
+
+ is JsonArray -> {
+ return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() })
+ }
+
+ is JsonPrimitive -> {
+ if (this.isString)
+ return kotlinx.serialization.json.JsonPrimitive(this.asString)
+ if (this.isBoolean)
+ return kotlinx.serialization.json.JsonPrimitive(this.asBoolean)
+ return kotlinx.serialization.json.JsonPrimitive(this.asNumber)
+ }
+
+ else -> error("Unknown json variant $this")
+ }
+}
+
+fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement {
+ when (this) {
+ is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE
+ is kotlinx.serialization.json.JsonPrimitive -> {
+ if (this.isString)
+ return JsonPrimitive(this.content)
+ if (this.content == "true")
+ return JsonPrimitive(true)
+ if (this.content == "false")
+ return JsonPrimitive(false)
+ return JsonPrimitive(LazilyParsedNumber(this.content))
+ }
+
+ is kotlinx.serialization.json.JsonObject -> {
+ val obj = JsonObject()
+ for ((k, v) in this) {
+ obj.add(k, v.intoGson())
+ }
+ return obj
+ }
+
+ is kotlinx.serialization.json.JsonArray -> {
+ val arr = JsonArray()
+ for (v in this) {
+ arr.add(v.intoGson())
+ }
+ return arr
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt
new file mode 100644
index 0000000..53df184
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TintOverrides.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import moe.nea.firmament.util.ErrorUtil
+
+data class TintOverrides(
+ val layerMap: Map<Int, TintOverride> = mapOf()
+) {
+ val hasOverrides by lazy { layerMap.values.any { it !is Reset } }
+
+ companion object {
+ val EMPTY = TintOverrides()
+ private val threadLocal = object : ThreadLocal<TintOverrides>() {}
+ fun enter(overrides: TintOverrides?) {
+ ErrorUtil.softCheck("Double entered tintOverrides",
+ threadLocal.get() == null)
+ threadLocal.set(overrides ?: EMPTY)
+ }
+
+ fun exit(overrides: TintOverrides?) {
+ ErrorUtil.softCheck("Exited with non matching enter tintOverrides",
+ threadLocal.get() == (overrides ?: EMPTY))
+ threadLocal.remove()
+ }
+
+ fun getCurrentOverrides(): TintOverrides {
+ return ErrorUtil.notNullOr(threadLocal.get(), "Got current tintOverrides without entering") {
+ EMPTY
+ }
+ }
+
+ fun parse(jsonObject: JsonObject): TintOverrides {
+ val map = mutableMapOf<Int, TintOverride>()
+ for ((key, value) in jsonObject.entrySet()) {
+ val layerIndex =
+ ErrorUtil.notNullOr(key.toIntOrNull(),
+ "Unknown layer index $value. Should be integer") { continue }
+ if (value.isJsonNull) {
+ map[layerIndex] = Reset
+ continue
+ }
+ val override = (value as? JsonPrimitive)
+ ?.takeIf(JsonPrimitive::isNumber)
+ ?.asInt
+ ?.let(TintOverrides::Fixed)
+ if (override == null) {
+ ErrorUtil.softError("Invalid tint override for a layer: $value")
+ continue
+ }
+ map[layerIndex] = override
+ }
+ return TintOverrides(map)
+ }
+ }
+
+ fun mergeWithParent(parent: TintOverrides): TintOverrides {
+ val mergedMap = parent.layerMap.toMutableMap()
+ mergedMap.putAll(this.layerMap)
+ return TintOverrides(mergedMap)
+ }
+
+ fun hasOverrides(): Boolean = hasOverrides
+ fun getOverride(tintIndex: Int): Int? {
+ return when (val tint = layerMap[tintIndex]) {
+ is Reset -> null
+ is Fixed -> tint.color
+ null -> null
+ }
+ }
+
+ sealed interface TintOverride
+ data object Reset : TintOverride
+ data class Fixed(val color: Int) : TintOverride
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt
new file mode 100644
index 0000000..7e0ddb1
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt
@@ -0,0 +1,19 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import net.minecraft.item.ItemStack
+
+object AlwaysPredicate : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return true
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ return AlwaysPredicate
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt
new file mode 100644
index 0000000..99abaaa
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt
@@ -0,0 +1,28 @@
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import net.minecraft.item.ItemStack
+
+class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return children.all { it.test(stack) }
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ val children =
+ (jsonElement as JsonArray)
+ .flatMap {
+ CustomModelOverrideParser.parsePredicates(it as JsonObject)
+ }
+ .toTypedArray()
+ return AndPredicate(children)
+ }
+
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt
new file mode 100644
index 0000000..04c7a2b
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt
@@ -0,0 +1,22 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.features.texturepack.StringMatcher
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+
+data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ val display = stack.displayNameAccordingToNbt
+ return stringMatcher.matches(display)
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ return DisplayNamePredicate(StringMatcher.parse(jsonElement))
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt
new file mode 100644
index 0000000..3c8023d
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt
@@ -0,0 +1,271 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.features.texturepack.StringMatcher
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtDouble
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtFloat
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtLong
+import net.minecraft.nbt.NbtShort
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.extraAttributes
+
+fun interface NbtMatcher {
+ fun matches(nbt: NbtElement): Boolean
+
+ object Parser {
+ fun parse(jsonElement: JsonElement): NbtMatcher? {
+ if (jsonElement is JsonPrimitive) {
+ if (jsonElement.isString) {
+ val string = jsonElement.asString
+ return MatchStringExact(string)
+ }
+ if (jsonElement.isNumber) {
+ return MatchNumberExact(jsonElement.asLong) //TODO: parse generic number
+ }
+ }
+ if (jsonElement is JsonObject) {
+ var encounteredParser: NbtMatcher? = null
+ for (entry in ExclusiveParserType.entries) {
+ val data = jsonElement[entry.key] ?: continue
+ if (encounteredParser != null) {
+ // TODO: warn
+ return null
+ }
+ encounteredParser = entry.parse(data) ?: return null
+ }
+ return encounteredParser
+ }
+ return null
+ }
+
+ enum class ExclusiveParserType(val key: String) {
+ STRING("string") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return MatchString(StringMatcher.parse(element))
+ }
+ },
+ INT("int") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asInt },
+ { (it as? NbtInt)?.intValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ FLOAT("float") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asFloat },
+ { (it as? NbtFloat)?.floatValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ DOUBLE("double") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asDouble },
+ { (it as? NbtDouble)?.doubleValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ LONG("long") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asLong },
+ { (it as? NbtLong)?.longValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ SHORT("short") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asShort },
+ { (it as? NbtShort)?.shortValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ BYTE("byte") {
+ override fun parse(element: JsonElement): NbtMatcher? {
+ return parseGenericNumber(element,
+ { it.asByte },
+ { (it as? NbtByte)?.byteValue() },
+ { a, b ->
+ if (a == b) Comparison.EQUAL
+ else if (a < b) Comparison.LESS_THAN
+ else Comparison.GREATER
+ })
+ }
+ },
+ ;
+
+ abstract fun parse(element: JsonElement): NbtMatcher?
+ }
+
+ enum class Comparison {
+ LESS_THAN, EQUAL, GREATER
+ }
+
+ inline fun <T : Any> parseGenericNumber(
+ jsonElement: JsonElement,
+ primitiveExtractor: (JsonPrimitive) -> T?,
+ crossinline nbtExtractor: (NbtElement) -> T?,
+ crossinline compare: (T, T) -> Comparison
+ ): NbtMatcher? {
+ if (jsonElement is JsonPrimitive) {
+ val expected = primitiveExtractor(jsonElement) ?: return null
+ return NbtMatcher {
+ val actual = nbtExtractor(it) ?: return@NbtMatcher false
+ compare(actual, expected) == Comparison.EQUAL
+ }
+ }
+ if (jsonElement is JsonObject) {
+ val minElement = jsonElement.getAsJsonPrimitive("min")
+ val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null
+ val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false
+ val maxElement = jsonElement.getAsJsonPrimitive("max")
+ val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null
+ val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true
+ if (min == null && max == null) return null
+ return NbtMatcher {
+ val actual = nbtExtractor(it) ?: return@NbtMatcher false
+ if (max != null) {
+ val comp = compare(actual, max)
+ if (comp == Comparison.GREATER) return@NbtMatcher false
+ if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false
+ }
+ if (min != null) {
+ val comp = compare(actual, min)
+ if (comp == Comparison.LESS_THAN) return@NbtMatcher false
+ if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false
+ }
+ return@NbtMatcher true
+ }
+ }
+ return null
+
+ }
+ }
+
+ class MatchNumberExact(val number: Long) : NbtMatcher {
+ override fun matches(nbt: NbtElement): Boolean {
+ return when (nbt) {
+ is NbtByte -> nbt.byteValue().toLong() == number
+ is NbtInt -> nbt.intValue().toLong() == number
+ is NbtShort -> nbt.shortValue().toLong() == number
+ is NbtLong -> nbt.longValue().toLong() == number
+ else -> false
+ }
+ }
+
+ }
+
+ class MatchStringExact(val string: String) : NbtMatcher {
+ override fun matches(nbt: NbtElement): Boolean {
+ return nbt is NbtString && nbt.asString() == string
+ }
+
+ override fun toString(): String {
+ return "MatchNbtStringExactly($string)"
+ }
+ }
+
+ class MatchString(val string: StringMatcher) : NbtMatcher {
+ override fun matches(nbt: NbtElement): Boolean {
+ return nbt is NbtString && string.matches(nbt.asString())
+ }
+
+ override fun toString(): String {
+ return "MatchNbtString($string)"
+ }
+ }
+}
+
+data class ExtraAttributesPredicate(
+ val path: NbtPrism,
+ val matcher: NbtMatcher,
+) : FirmamentModelPredicate {
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
+ if (jsonElement !is JsonObject) return null
+ val path = jsonElement.get("path") ?: return null
+ val pathSegments = if (path is JsonArray) {
+ path.map { (it as JsonPrimitive).asString }
+ } else if (path is JsonPrimitive && path.isString) {
+ path.asString.split(".")
+ } else return null
+ val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
+ ?: return null
+ return ExtraAttributesPredicate(NbtPrism(pathSegments), matcher)
+ }
+ }
+
+ override fun test(stack: ItemStack): Boolean {
+ return path.access(stack.extraAttributes)
+ .any { matcher.matches(it) }
+ }
+}
+
+class NbtPrism(val path: List<String>) {
+ override fun toString(): String {
+ return "Prism($path)"
+ }
+ fun access(root: NbtElement): Collection<NbtElement> {
+ var rootSet = mutableListOf(root)
+ var switch = mutableListOf<NbtElement>()
+ for (pathSegment in path) {
+ if (pathSegment == ".") continue
+ for (element in rootSet) {
+ if (element is NbtList) {
+ if (pathSegment == "*")
+ switch.addAll(element)
+ val index = pathSegment.toIntOrNull() ?: continue
+ if (index !in element.indices) continue
+ switch.add(element[index])
+ }
+ if (element is NbtCompound) {
+ if (pathSegment == "*")
+ element.keys.mapTo(switch) { element.get(it)!! }
+ switch.add(element.get(pathSegment) ?: continue)
+ }
+ }
+ val temp = switch
+ switch = rootSet
+ rootSet = temp
+ switch.clear()
+ }
+ return rootSet
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt
new file mode 100644
index 0000000..3cb80c7
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt
@@ -0,0 +1,34 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.MC
+
+class ItemPredicate(
+ val item: Item
+) : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return stack.item == item
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): ItemPredicate? {
+ if (jsonElement is JsonPrimitive && jsonElement.isString) {
+ val itemKey = RegistryKey.of(RegistryKeys.ITEM,
+ Identifier.tryParse(jsonElement.asString)
+ ?: return null)
+ return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null)
+ }
+ return null
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt
new file mode 100644
index 0000000..f0b4737
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt
@@ -0,0 +1,22 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.features.texturepack.StringMatcher
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+
+class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate {
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ return LorePredicate(StringMatcher.parse(jsonElement))
+ }
+ }
+
+ override fun test(stack: ItemStack): Boolean {
+ val lore = stack.loreAccordingToNbt
+ return lore.any { matcher.matches(it) }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt
new file mode 100644
index 0000000..4986ad9
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt
@@ -0,0 +1,21 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import net.minecraft.item.ItemStack
+
+class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return children.none { it.test(stack) }
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ return NotPredicate(CustomModelOverrideParser.parsePredicates(jsonElement as JsonObject).toTypedArray())
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt
new file mode 100644
index 0000000..b0d5178
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NumberMatcher.kt
@@ -0,0 +1,124 @@
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+import moe.nea.firmament.util.useMatch
+
+abstract class NumberMatcher {
+ abstract fun test(number: Number): Boolean
+
+
+ companion object {
+ fun parse(jsonElement: JsonElement): NumberMatcher? {
+ if (jsonElement is JsonPrimitive) {
+ if (jsonElement.isString) {
+ val string = jsonElement.asString
+ return parseRange(string) ?: parseOperator(string)
+ }
+ if (jsonElement.isNumber) {
+ val number = jsonElement.asNumber
+ val hasDecimals = (number.toString().contains("."))
+ return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble())
+ }
+ }
+ return null
+ }
+
+ private val intervalSpec =
+ "(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
+ .toPattern()
+
+ fun parseRange(string: String): RangeMatcher? {
+ intervalSpec.useMatch<Nothing>(string) {
+ // Open in the set-theory sense, meaning does not include its end.
+ val beginningOpen = group("beginningOpen") == "("
+ val endingOpen = group("endingOpen") == ")"
+ val beginning = group("beginning")?.toDouble()
+ val ending = group("ending")?.toDouble()
+ return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
+ }
+ return null
+ }
+
+ enum class Operator(val operator: String) {
+ LESS("<") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult < 0
+ }
+ },
+ LESS_EQUALS("<=") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult <= 0
+ }
+ },
+ GREATER(">") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult > 0
+ }
+ },
+ GREATER_EQUALS(">=") {
+ override fun matches(comparisonResult: Int): Boolean {
+ return comparisonResult >= 0
+ }
+ },
+ ;
+
+ abstract fun matches(comparisonResult: Int): Boolean
+ }
+
+ private val operatorPattern =
+ "(?<operator>${Operator.entries.joinToString("|") { it.operator }})(?<value>[0-9.]+)".toPattern()
+
+ fun parseOperator(string: String): OperatorMatcher? {
+ return operatorPattern.useMatch(string) {
+ val operatorName = group("operator")
+ val operator = Operator.entries.find { it.operator == operatorName }!!
+ val value = group("value").toDouble()
+ OperatorMatcher(operator, value)
+ }
+ }
+
+ data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ return operator.matches(number.toDouble().compareTo(value))
+ }
+ }
+
+
+ data class MatchNumberExact(val number: Number) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ return when (this.number) {
+ is Double -> number.toDouble() == this.number.toDouble()
+ else -> number.toLong() == this.number.toLong()
+ }
+ }
+ }
+
+ data class RangeMatcher(
+ val beginning: Double?,
+ val beginningInclusive: Boolean,
+ val ending: Double?,
+ val endingInclusive: Boolean,
+ ) : NumberMatcher() {
+ override fun test(number: Number): Boolean {
+ val value = number.toDouble()
+ if (beginning != null) {
+ if (beginningInclusive) {
+ if (value < beginning) return false
+ } else {
+ if (value <= beginning) return false
+ }
+ }
+ if (ending != null) {
+ if (endingInclusive) {
+ if (value > ending) return false
+ } else {
+ if (value >= ending) return false
+ }
+ }
+ return true
+ }
+ }
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt
new file mode 100644
index 0000000..e3093cd
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt
@@ -0,0 +1,29 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import net.minecraft.item.ItemStack
+
+class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
+ override fun test(stack: ItemStack): Boolean {
+ return children.any { it.test(stack) }
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
+ val children =
+ (jsonElement as JsonArray)
+ .flatMap {
+ CustomModelOverrideParser.parsePredicates(it as JsonObject)
+ }
+ .toTypedArray()
+ return OrPredicate(children)
+ }
+
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt
new file mode 100644
index 0000000..b30b7c9
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt
@@ -0,0 +1,66 @@
+
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.features.texturepack.RarityMatcher
+import moe.nea.firmament.features.texturepack.StringMatcher
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.repo.ExpLadders
+import moe.nea.firmament.util.petData
+
+data class PetPredicate(
+ val petId: StringMatcher?,
+ val tier: RarityMatcher?,
+ val exp: NumberMatcher?,
+ val candyUsed: NumberMatcher?,
+ val level: NumberMatcher?,
+) : FirmamentModelPredicate {
+
+ override fun test(stack: ItemStack): Boolean {
+ val petData = stack.petData ?: return false
+ if (petId != null) {
+ if (!petId.matches(petData.type)) return false
+ }
+ if (exp != null) {
+ if (!exp.test(petData.exp)) return false
+ }
+ if (candyUsed != null) {
+ if (!candyUsed.test(petData.candyUsed)) return false
+ }
+ if (tier != null) {
+ if (!tier.match(petData.tier)) return false
+ }
+ val levelData by lazy(LazyThreadSafetyMode.NONE) {
+ ExpLadders.getExpLadder(petData.type, petData.tier)
+ .getPetLevel(petData.exp)
+ }
+ if (level != null) {
+ if (!level.test(levelData.currentLevel)) return false
+ }
+ return true
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
+ if (jsonElement.isJsonPrimitive) {
+ return PetPredicate(StringMatcher.Equals(jsonElement.asString, false), null, null, null, null)
+ }
+ if (jsonElement !is JsonObject) return null
+ val idMatcher = jsonElement["id"]?.let(StringMatcher::parse)
+ val expMatcher = jsonElement["exp"]?.let(NumberMatcher::parse)
+ val levelMatcher = jsonElement["level"]?.let(NumberMatcher::parse)
+ val candyMatcher = jsonElement["candyUsed"]?.let(NumberMatcher::parse)
+ val tierMatcher = jsonElement["tier"]?.let(RarityMatcher::parse)
+ return PetPredicate(
+ idMatcher,
+ tierMatcher,
+ expMatcher,
+ candyMatcher,
+ levelMatcher,
+ )
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java
new file mode 100644
index 0000000..cf5cf59
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ApplyHeadModelInItemRenderer.java
@@ -0,0 +1,11 @@
+
+package moe.nea.firmament.mixins.custommodels;
+
+import net.minecraft.client.item.ItemModelManager;
+import org.spongepowered.asm.mixin.Mixin;
+
+@Mixin(ItemModelManager.class)
+public class ApplyHeadModelInItemRenderer {
+ // TODO: replace head_model with a condition model (if possible, automatically)
+ // TODO: ItemAsset.CODEC should upgrade partials
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java
new file mode 100644
index 0000000..fede766
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/CustomSkullTexturePatch.java
@@ -0,0 +1,26 @@
+
+
+package moe.nea.firmament.mixins.custommodels;
+
+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 net.minecraft.component.type.ProfileComponent;
+import net.minecraft.util.Identifier;
+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 CustomSkullTexturePatch {
+ @Inject(
+ method = "getRenderLayer(Lnet/minecraft/block/SkullBlock$SkullType;Lnet/minecraft/component/type/ProfileComponent;Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;",
+ at = @At("HEAD"),
+ cancellable = true
+ )
+ private static void onGetRenderLayer(SkullBlock.SkullType type, ProfileComponent profile, Identifier texture, CallbackInfoReturnable<RenderLayer> cir) {
+ CustomSkyBlockTextures.INSTANCE.modifySkullTexture(type, profile, cir);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java
new file mode 100644
index 0000000..669da63
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchArmorTexture.java
@@ -0,0 +1,30 @@
+
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides;
+import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer;
+import net.minecraft.component.ComponentType;
+import net.minecraft.component.type.EquippableComponent;
+import net.minecraft.entity.EquipmentSlot;
+import net.minecraft.item.ItemStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(ArmorFeatureRenderer.class)
+public class PatchArmorTexture {
+ @ModifyExpressionValue(
+ method = "renderArmor",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;"))
+ private Object overrideLayers(
+ Object original, @Local(argsOnly = true) ItemStack itemStack, @Local(argsOnly = true) EquipmentSlot slot
+ ) {
+ var overrides = CustomGlobalArmorOverrides.overrideArmor(itemStack, slot);
+ return overrides.orElse((EquippableComponent) original);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java
new file mode 100644
index 0000000..81ea6cd
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java
@@ -0,0 +1,23 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides;
+import net.minecraft.client.render.entity.equipment.EquipmentModel;
+import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
+import net.minecraft.client.render.entity.equipment.EquipmentRenderer;
+import net.minecraft.item.equipment.EquipmentAsset;
+import net.minecraft.registry.RegistryKey;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+// TODO: auto import legacy models, maybe!!! in a later patch tho
+@Mixin(EquipmentRenderer.class)
+public class PatchLegacyArmorLayerSupport {
+ @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/registry/RegistryKey;)Lnet/minecraft/client/render/entity/equipment/EquipmentModel;"))
+ private EquipmentModel patchModelLayers(EquipmentModelLoader instance, RegistryKey<EquipmentAsset> assetKey, Operation<EquipmentModel> original) {
+ var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue());
+ if (modelOverride != null) return modelOverride;
+ return original.call(instance, assetKey);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java
new file mode 100644
index 0000000..bbabeb5
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReferenceCustomModelsPatch.java
@@ -0,0 +1,31 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.events.BakeExtraModelsEvent;
+import net.minecraft.client.item.ItemAssetsLoader;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.BlockStatesLoader;
+import net.minecraft.client.render.model.ReferencedModelsCollector;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.util.Map;
+
+@Mixin(BakedModelManager.class)
+public abstract class ReferenceCustomModelsPatch {
+ @Inject(method = "collect", at = @At("RETURN"))
+ private static void addFirmamentReferencedModels(
+ UnbakedModel missingModel, Map<Identifier, UnbakedModel> models, BlockStatesLoader.BlockStateDefinition blockStates, ItemAssetsLoader.Result itemAssets, CallbackInfoReturnable<ReferencedModelsCollector> cir,
+ @Local ReferencedModelsCollector collector) {
+ // TODO: Insert fake models based on firmskyblock models for a smoother transition
+
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java
new file mode 100644
index 0000000..9401889
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java
@@ -0,0 +1,27 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.sound.BlockSoundGroup;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.math.BlockPos;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(WorldRenderer.class)
+public class ReplaceBlockBreakSoundPatch {
+// Sadly hypixel does not send a world event here and instead plays the sound on the server directly
+// @WrapOperation(method = "processWorldEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/sound/BlockSoundGroup;getBreakSound()Lnet/minecraft/sound/SoundEvent;"))
+// private SoundEvent replaceBreakSoundEvent(BlockSoundGroup instance, Operation<SoundEvent> original,
+// @Local(argsOnly = true) BlockPos pos, @Local BlockState blockState) {
+// var replacement = CustomBlockTextures.getReplacement(blockState, pos);
+// if (replacement != null && replacement.getSound() != null) {
+// return SoundEvent.of(replacement.getSound());
+// }
+// return original.call(instance);
+// }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java
new file mode 100644
index 0000000..f9a1d0d
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java
@@ -0,0 +1,30 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.network.ClientPlayerInteractionManager;
+import net.minecraft.client.sound.PositionedSoundInstance;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
+import net.minecraft.util.math.BlockPos;
+import net.minecraft.util.math.random.Random;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(ClientPlayerInteractionManager.class)
+public class ReplaceBlockHitSoundPatch {
+ @WrapOperation(method = "updateBlockBreakingProgress", at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;"))
+ private PositionedSoundInstance replaceSound(
+ SoundEvent sound, SoundCategory category, float volume, float pitch,
+ Random random, BlockPos pos, Operation<PositionedSoundInstance> original,
+ @Local BlockState blockState) {
+ var replacement = CustomBlockTextures.getReplacement(blockState, pos);
+ if (replacement != null && replacement.getSound() != null) {
+ sound = SoundEvent.of(replacement.getSound());
+ }
+ return original.call(sound, category, volume, pitch, random, pos);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java
new file mode 100644
index 0000000..711b2af
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java
@@ -0,0 +1,38 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.block.BlockModels;
+import net.minecraft.client.render.block.BlockRenderManager;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.util.math.BlockPos;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(BlockRenderManager.class)
+public class ReplaceBlockRenderManagerBlockModel {
+ @WrapOperation(method = "renderBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;"))
+ private BakedModel replaceModelInRenderBlock(
+ BlockRenderManager instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) {
+ var replacement = CustomBlockTextures.getReplacementModel(state, pos);
+ if (replacement != null) return replacement;
+ CustomBlockTextures.enterFallbackCall();
+ var fallback = original.call(instance, state);
+ CustomBlockTextures.exitFallbackCall();
+ return fallback;
+ }
+
+ @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;"))
+ private BakedModel replaceModelInRenderDamage(
+ BlockModels instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) {
+ var replacement = CustomBlockTextures.getReplacementModel(state, pos);
+ if (replacement != null) return replacement;
+ CustomBlockTextures.enterFallbackCall();
+ var fallback = original.call(instance, state);
+ CustomBlockTextures.exitFallbackCall();
+ return fallback;
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java
new file mode 100644
index 0000000..53ab74a
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java
@@ -0,0 +1,21 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.block.BlockModels;
+import net.minecraft.client.render.model.BakedModel;
+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(BlockModels.class)
+public class ReplaceFallbackBlockModel {
+ // TODO: add check to BlockDustParticle
+ @Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
+ private void getModel(BlockState state, CallbackInfoReturnable<BakedModel> cir) {
+ var replacement = CustomBlockTextures.getReplacementModel(state, null);
+ if (replacement != null)
+ cir.setReturnValue(replacement);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
new file mode 100644
index 0000000..0863caa
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
@@ -0,0 +1,24 @@
+package moe.nea.firmament.mixins.custommodels;
+
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.events.CustomItemModelEvent;
+import net.minecraft.client.item.ItemModelManager;
+import net.minecraft.component.ComponentType;
+import net.minecraft.item.ItemStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(ItemModelManager.class)
+public class ReplaceItemModelPatch {
+ @WrapOperation(
+ method = "update(Lnet/minecraft/client/render/item/ItemRenderState;Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;Lnet/minecraft/world/World;Lnet/minecraft/entity/LivingEntity;I)V",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;"))
+ private Object replaceItemModelByIdentifier(ItemStack instance, ComponentType componentType, Operation<Object> original) {
+ var override = CustomItemModelEvent.getModelIdentifier(instance);
+ if (override != null)
+ return override;
+ return original.call(instance, componentType);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java
new file mode 100644
index 0000000..e4834e9
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java
@@ -0,0 +1,48 @@
+package moe.nea.firmament.mixins.custommodels;
+
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.features.texturepack.CustomTextColors;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.AnvilScreen;
+import net.minecraft.client.gui.screen.ingame.BeaconScreen;
+import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.client.gui.screen.ingame.MerchantScreen;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin({HandledScreen.class, InventoryScreen.class, CreativeInventoryScreen.class, MerchantScreen.class,
+ AnvilScreen.class, BeaconScreen.class})
+public class ReplaceTextColorInHandledScreen {
+
+ // To my future self: double check those mixins, but don't be too concerned about errors. Some of the wrapopertions
+ // only apply in some of the specified subclasses.
+
+ @WrapOperation(
+ method = "drawForeground",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
+ expect = 0,
+ require = 0)
+ private int replaceTextColorWithVariableShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
+ return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color), shadow);
+ }
+
+ @WrapOperation(
+ method = "drawForeground",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"),
+ expect = 0,
+ require = 0)
+ private int replaceTextColorWithShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) {
+ return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color));
+ }
+
+}