aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/texturepack
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
committerLinnea Gräf <nea@nea.moe>2024-08-28 19:04:24 +0200
commitd2f240ff0ca0d27f417f837e706c781a98c31311 (patch)
tree0db7aff6cc14deaf36eed83889d59fd6b3a6f599 /src/main/kotlin/features/texturepack
parenta6906308163aa3b2d18fa1dc1aa71ac9bbcc83ab (diff)
downloadFirmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.gz
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.tar.bz2
Firmament-d2f240ff0ca0d27f417f837e706c781a98c31311.zip
Refactor source layout
Introduce compat source sets and move all kotlin sources to the main directory [no changelog]
Diffstat (limited to 'src/main/kotlin/features/texturepack')
-rw-r--r--src/main/kotlin/features/texturepack/AlwaysPredicate.kt17
-rw-r--r--src/main/kotlin/features/texturepack/AndPredicate.kt26
-rw-r--r--src/main/kotlin/features/texturepack/BakedModelExtra.kt9
-rw-r--r--src/main/kotlin/features/texturepack/BakedOverrideData.kt8
-rw-r--r--src/main/kotlin/features/texturepack/CustomBlockTextures.kt295
-rw-r--r--src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt106
-rw-r--r--src/main/kotlin/features/texturepack/CustomGlobalTextures.kt167
-rw-r--r--src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt74
-rw-r--r--src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt114
-rw-r--r--src/main/kotlin/features/texturepack/DisplayNamePredicate.kt22
-rw-r--r--src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt268
-rw-r--r--src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt8
-rw-r--r--src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt8
-rw-r--r--src/main/kotlin/features/texturepack/ItemPredicate.kt32
-rw-r--r--src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt10
-rw-r--r--src/main/kotlin/features/texturepack/LorePredicate.kt19
-rw-r--r--src/main/kotlin/features/texturepack/ModelOverrideData.kt7
-rw-r--r--src/main/kotlin/features/texturepack/ModelOverrideFilterSet.kt19
-rw-r--r--src/main/kotlin/features/texturepack/NotPredicate.kt18
-rw-r--r--src/main/kotlin/features/texturepack/NumberMatcher.kt125
-rw-r--r--src/main/kotlin/features/texturepack/OrPredicate.kt26
-rw-r--r--src/main/kotlin/features/texturepack/PetPredicate.kt66
-rw-r--r--src/main/kotlin/features/texturepack/RarityMatcher.kt69
-rw-r--r--src/main/kotlin/features/texturepack/StringMatcher.kt159
24 files changed, 1672 insertions, 0 deletions
diff --git a/src/main/kotlin/features/texturepack/AlwaysPredicate.kt b/src/main/kotlin/features/texturepack/AlwaysPredicate.kt
new file mode 100644
index 0000000..4dd28df
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/AlwaysPredicate.kt
@@ -0,0 +1,17 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+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/main/kotlin/features/texturepack/AndPredicate.kt b/src/main/kotlin/features/texturepack/AndPredicate.kt
new file mode 100644
index 0000000..55a4f32
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/AndPredicate.kt
@@ -0,0 +1,26 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+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/main/kotlin/features/texturepack/BakedModelExtra.kt b/src/main/kotlin/features/texturepack/BakedModelExtra.kt
new file mode 100644
index 0000000..ae1f6d5
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/BakedModelExtra.kt
@@ -0,0 +1,9 @@
+
+package moe.nea.firmament.features.texturepack
+
+import net.minecraft.client.render.model.BakedModel
+
+interface BakedModelExtra {
+ fun getHeadModel_firmament(): BakedModel?
+ fun setHeadModel_firmament(headModel: BakedModel?)
+}
diff --git a/src/main/kotlin/features/texturepack/BakedOverrideData.kt b/src/main/kotlin/features/texturepack/BakedOverrideData.kt
new file mode 100644
index 0000000..c012883
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/BakedOverrideData.kt
@@ -0,0 +1,8 @@
+
+package moe.nea.firmament.features.texturepack
+
+interface BakedOverrideData {
+ fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
+ fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
+
+}
diff --git a/src/main/kotlin/features/texturepack/CustomBlockTextures.kt b/src/main/kotlin/features/texturepack/CustomBlockTextures.kt
new file mode 100644
index 0000000..0f2c2e6
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/CustomBlockTextures.kt
@@ -0,0 +1,295 @@
+@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 {
+ Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader").getConstructor().newInstance() as Runnable
+ }.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) }
+ }
+
+ 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.getWrapperOrThrow(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/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt b/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt
new file mode 100644
index 0000000..23577ee
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt
@@ -0,0 +1,106 @@
+
+@file:UseSerializers(IdentifierSerializer::class)
+
+package moe.nea.firmament.features.texturepack
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.UseSerializers
+import net.minecraft.item.ArmorMaterial
+import net.minecraft.item.ItemStack
+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.events.subscription.SubscriptionOwner
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
+import moe.nea.firmament.util.IdentifierSerializer
+import moe.nea.firmament.util.IdentityCharacteristics
+import moe.nea.firmament.util.computeNullableFunction
+import moe.nea.firmament.util.skyBlockId
+
+object CustomGlobalArmorOverrides : SubscriptionOwner {
+ @Serializable
+ data class ArmorOverride(
+ @SerialName("item_ids")
+ val itemIds: List<String>,
+ val layers: List<ArmorOverrideLayer>,
+ val overrides: List<ArmorOverrideOverride> = listOf(),
+ ) {
+ @Transient
+ val bakedLayers = bakeLayers(layers)
+ }
+
+ fun bakeLayers(layers: List<ArmorOverrideLayer>): List<ArmorMaterial.Layer> {
+ return layers.map { ArmorMaterial.Layer(it.identifier, it.suffix, it.tint) }
+ }
+
+ @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>,
+ ) {
+ @Transient
+ val bakedLayers = bakeLayers(layers)
+ }
+
+ override val delegateFeature: FirmamentFeature
+ get() = CustomSkyBlockTextures
+
+ val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
+
+ @JvmStatic
+ fun overrideArmor(stack: ItemStack): List<ArmorMaterial.Layer>? {
+ if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null
+ return overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
+ val id = stack.skyBlockId ?: return@computeNullableFunction null
+ val override = overrides[id.neuItem] ?: return@computeNullableFunction null
+ for (suboverride in override.overrides) {
+ if (suboverride.predicate.test(stack)) {
+ return@computeNullableFunction suboverride.bakedLayers
+ }
+ }
+ return@computeNullableFunction override.bakedLayers
+ }
+ }
+
+ var overrides: Map<String, ArmorOverride> = mapOf()
+
+ @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()
+ return associatedMap
+ }
+
+ override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
+ overrides = prepared
+ }
+ })
+ }
+
+}
diff --git a/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt b/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
new file mode 100644
index 0000000..d64c844
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
@@ -0,0 +1,167 @@
+
+@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class)
+
+package moe.nea.firmament.features.texturepack
+
+
+import java.util.concurrent.CompletableFuture
+import org.slf4j.LoggerFactory
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.UseSerializers
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.client.render.item.ItemModels
+import net.minecraft.client.render.model.BakedModel
+import net.minecraft.client.util.ModelIdentifier
+import net.minecraft.item.ItemStack
+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.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.IdentifierSerializer
+import moe.nea.firmament.util.IdentityCharacteristics
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.computeNullableFunction
+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?) {
+ this.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 ->
+ logger.error("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 {
+ logger.error("Failed to locate screen filter at $key")
+ }
+ val screenFilter =
+ Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
+ .getOrElse { ex ->
+ logger.error("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) }
+ }
+
+ val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
+
+ @JvmStatic
+ fun replaceGlobalModel(
+ models: ItemModels,
+ stack: ItemStack,
+ cir: CallbackInfoReturnable<BakedModel>
+ ) {
+ val value = overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
+ for (guiClassOverride in matchingOverrides) {
+ for (override in guiClassOverride.overrides) {
+ if (override.predicate.test(stack)) {
+ return@computeNullableFunction models.modelManager.getModel(
+ ModelIdentifier(override.model, "inventory"))
+ }
+ }
+ }
+ null
+ }
+ if (value != null)
+ cir.returnValue = value
+ }
+
+
+}
diff --git a/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt b/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt
new file mode 100644
index 0000000..a4e7c02
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt
@@ -0,0 +1,74 @@
+
+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 net.minecraft.item.ItemStack
+import net.minecraft.util.Identifier
+
+object CustomModelOverrideParser {
+ 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(parsePredicates(json).toTypedArray())
+ }
+
+ override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) {
+ TODO("Cannot serialize firmament predicates")
+ }
+ }
+
+ 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()) {
+