aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin/features/texturepack
diff options
context:
space:
mode:
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()) {
+ 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()
+ }
+}
diff --git a/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt b/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
new file mode 100644
index 0000000..dec6046
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
@@ -0,0 +1,114 @@
+package moe.nea.firmament.features.texturepack
+
+import com.mojang.authlib.minecraft.MinecraftProfileTexture
+import com.mojang.authlib.properties.Property
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
+import net.minecraft.block.SkullBlock
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.util.ModelIdentifier
+import net.minecraft.component.type.ProfileComponent
+import net.minecraft.util.Identifier
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.BakeExtraModelsEvent
+import moe.nea.firmament.events.CustomItemModelEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.IdentityCharacteristics
+import moe.nea.firmament.util.item.decodeProfileTextureProperty
+import moe.nea.firmament.util.skyBlockId
+
+object CustomSkyBlockTextures : FirmamentFeature {
+ override val identifier: String
+ get() = "custom-skyblock-textures"
+
+ object TConfig : ManagedConfig(identifier) {
+ val enabled by toggle("enabled") { true }
+ val skullsEnabled by toggle("skulls-enabled") { true }
+ val cacheDuration by integer("cache-duration", 0, 20) { 1 }
+ val enableModelOverrides by toggle("model-overrides") { true }
+ val enableArmorOverrides by toggle("armor-overrides") { true }
+ val enableBlockOverrides by toggle("block-overrides") { true }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ @Subscribe
+ fun onTick(it: TickEvent) {
+ if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
+ // TODO: unify all of those caches somehow
+ CustomItemModelEvent.clearCache()
+ skullTextureCache.clear()
+ CustomGlobalTextures.overrideCache.clear()
+ CustomGlobalArmorOverrides.overrideCache.clear()
+ }
+ }
+
+ @Subscribe
+ fun bakeCustomFirmModels(event: BakeExtraModelsEvent) {
+ val resources =
+ MinecraftClient.getInstance().resourceManager.findResources("models/item"
+ ) { it: Identifier ->
+ "firmskyblock" == it.namespace && it.path
+ .endsWith(".json")
+ }
+ for (identifier in resources.keys) {
+ val modelId = ModelIdentifier.ofInventoryVariant(
+ Identifier.of(
+ "firmskyblock",
+ identifier.path.substring(
+ "models/item/".length,
+ identifier.path.length - ".json".length),
+ ))
+ event.addItemModel(modelId)
+ }
+ }
+
+ @Subscribe
+ fun onCustomModelId(it: CustomItemModelEvent) {
+ if (!TConfig.enabled) return
+ val id = it.itemStack.skyBlockId ?: return
+ it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path))
+ }
+
+ private val skullTextureCache = mutableMapOf<IdentityCharacteristics<ProfileComponent>, Any>()
+ private val sentinelPresentInvalid = Object()
+
+ 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 ic = IdentityCharacteristics(component)
+
+ val n = skullTextureCache.getOrPut(ic) {
+ val id = getSkullTexture(component) ?: return@getOrPut sentinelPresentInvalid
+ if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
+ return@getOrPut sentinelPresentInvalid
+ }
+ return@getOrPut id
+ }
+ if (n === sentinelPresentInvalid) return
+ cir.returnValue = RenderLayer.getEntityTranslucent(n as Identifier)
+ }
+}
diff --git a/src/main/kotlin/features/texturepack/DisplayNamePredicate.kt b/src/main/kotlin/features/texturepack/DisplayNamePredicate.kt
new file mode 100644
index 0000000..c89931e
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/DisplayNamePredicate.kt
@@ -0,0 +1,22 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.item.displayNameAccordingToNbt
+import moe.nea.firmament.util.item.loreAccordingToNbt
+
+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/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt b/src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt
new file mode 100644
index 0000000..4114f45
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/ExtraAttributesPredicate.kt
@@ -0,0 +1,268 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+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/main/kotlin/features/texturepack/FirmamentModelPredicate.kt b/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt
new file mode 100644
index 0000000..d11fec0
--- /dev/null
+++ b/src/main/kotlin/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/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt b/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt
new file mode 100644
index 0000000..3ed0c67
--- /dev/null
+++ b/src/main/kotlin/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/main/kotlin/features/texturepack/ItemPredicate.kt b/src/main/kotlin/features/texturepack/ItemPredicate.kt
new file mode 100644
index 0000000..4302b53
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/ItemPredicate.kt
@@ -0,0 +1,32 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+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/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt b/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt
new file mode 100644
index 0000000..ab9e27d
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt
@@ -0,0 +1,10 @@
+
+package moe.nea.firmament.features.texturepack
+
+import net.minecraft.util.Identifier
+
+interface JsonUnbakedModelFirmExtra {
+
+ fun setHeadModel_firmament(identifier: Identifier?)
+ fun getHeadModel_firmament(): Identifier?
+}
diff --git a/src/main/kotlin/features/texturepack/LorePredicate.kt b/src/main/kotlin/features/texturepack/LorePredicate.kt
new file mode 100644
index 0000000..13e3974
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/LorePredicate.kt
@@ -0,0 +1,19 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.util.item.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/main/kotlin/features/texturepack/ModelOverrideData.kt b/src/main/kotlin/features/texturepack/ModelOverrideData.kt
new file mode 100644
index 0000000..1585bd7
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/ModelOverrideData.kt
@@ -0,0 +1,7 @@
+
+package moe.nea.firmament.features.texturepack
+
+interface ModelOverrideData {
+ fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
+ fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
+}
diff --git a/src/main/kotlin/features/texturepack/ModelOverrideFilterSet.kt b/src/main/kotlin/features/texturepack/ModelOverrideFilterSet.kt
new file mode 100644
index 0000000..4ef8d06
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/ModelOverrideFilterSet.kt
@@ -0,0 +1,19 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import moe.nea.firmament.util.filter.IteratorFilterSet
+
+class ModelOverrideFilterSet(original: java.util.Set<Map.Entry<String, JsonElement>>) :
+ IteratorFilterSet<Map.Entry<String, JsonElement>>(original) {
+ companion object {
+ @JvmStatic
+ fun createFilterSet(set: java.util.Set<*>): java.util.Set<*> {
+ return ModelOverrideFilterSet(set as java.util.Set<Map.Entry<String, JsonElement>>) as java.util.Set<*>
+ }
+ }
+
+ override fun shouldKeepElement(element: Map.Entry<String, JsonElement>): Boolean {
+ return !element.key.startsWith("firmament:")
+ }
+}
diff --git a/src/main/kotlin/features/texturepack/NotPredicate.kt b/src/main/kotlin/features/texturepack/NotPredicate.kt
new file mode 100644
index 0000000..ecd67c3
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/NotPredicate.kt
@@ -0,0 +1,18 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+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/main/kotlin/features/texturepack/NumberMatcher.kt b/src/main/kotlin/features/texturepack/NumberMatcher.kt
new file mode 100644
index 0000000..7e6665f
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/NumberMatcher.kt
@@ -0,0 +1,125 @@
+
+package moe.nea.firmament.features.texturepack
+
+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? {
+ operatorPattern.useMatch<Nothing>(string) {
+ val operatorName = group("operator")
+ val operator = Operator.entries.find { it.operator == operatorName }!!
+ val value = group("value").toDouble()
+ return OperatorMatcher(operator, value)
+ }
+ return null
+ }
+
+ 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/main/kotlin/features/texturepack/OrPredicate.kt b/src/main/kotlin/features/texturepack/OrPredicate.kt
new file mode 100644
index 0000000..32f556b
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/OrPredicate.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 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/main/kotlin/features/texturepack/PetPredicate.kt b/src/main/kotlin/features/texturepack/PetPredicate.kt
new file mode 100644
index 0000000..5e5d750
--- /dev/null
+++ b/src/main/kotlin/features/texturepack/PetPredicate.kt
@@ -0,0 +1,66 @@
+
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.repo.ExpLadders
+import moe.nea.firmament.util.petData
+
+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,
+ )
+ }
+ }
+
+ override fun toString(): String {
+ return super.toString()
+ }
+}
diff --git a/src/main/kotlin/features/texturepack/RarityMatcher.kt b/src/main/kotlin/features/texturepack/RarityMatcher.kt
new file mode 100644
index 0000000..634a171
--- /dev/null
+++ b/src/main/kotlin/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/main/kotlin/features/texturepack/StringMatcher.kt b/src/main/kotlin/features/texturepack/StringMatcher.kt
new file mode 100644
index 0000000..5eb86ac
--- /dev/null
+++ b/src/main/kotlin/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, Companion.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
+ }
+ }
+}