feat: Add generic component matcher

This commit is contained in:
Linnea Gräf
2025-05-02 18:48:30 +02:00
parent b98272c364
commit afa128e8c6
4 changed files with 322 additions and 223 deletions

View File

@@ -17,6 +17,7 @@ import moe.nea.firmament.features.texturepack.predicates.AndPredicate
import moe.nea.firmament.features.texturepack.predicates.CastPredicate import moe.nea.firmament.features.texturepack.predicates.CastPredicate
import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate
import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate
import moe.nea.firmament.features.texturepack.predicates.GenericComponentPredicate
import moe.nea.firmament.features.texturepack.predicates.ItemPredicate import moe.nea.firmament.features.texturepack.predicates.ItemPredicate
import moe.nea.firmament.features.texturepack.predicates.LorePredicate import moe.nea.firmament.features.texturepack.predicates.LorePredicate
import moe.nea.firmament.features.texturepack.predicates.NotPredicate import moe.nea.firmament.features.texturepack.predicates.NotPredicate
@@ -63,6 +64,7 @@ object CustomModelOverrideParser {
registerPredicateParser("item", ItemPredicate.Parser) registerPredicateParser("item", ItemPredicate.Parser)
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
registerPredicateParser("pet", PetPredicate.Parser) registerPredicateParser("pet", PetPredicate.Parser)
registerPredicateParser("component", GenericComponentPredicate.Parser)
} }
private val neverPredicate = listOf( private val neverPredicate = listOf(

View File

@@ -22,194 +22,200 @@ import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.extraAttributes import moe.nea.firmament.util.extraAttributes
fun interface NbtMatcher { fun interface NbtMatcher {
fun matches(nbt: NbtElement): Boolean fun matches(nbt: NbtElement): Boolean
object Parser { object Parser {
fun parse(jsonElement: JsonElement): NbtMatcher? { fun parse(jsonElement: JsonElement): NbtMatcher? {
if (jsonElement is JsonPrimitive) { if (jsonElement is JsonPrimitive) {
if (jsonElement.isString) { if (jsonElement.isString) {
val string = jsonElement.asString val string = jsonElement.asString
return MatchStringExact(string) return MatchStringExact(string)
} }
if (jsonElement.isNumber) { if (jsonElement.isNumber) {
return MatchNumberExact(jsonElement.asLong) //TODO: parse generic number return MatchNumberExact(jsonElement.asLong) // TODO: parse generic number
} }
} }
if (jsonElement is JsonObject) { if (jsonElement is JsonObject) {
var encounteredParser: NbtMatcher? = null var encounteredParser: NbtMatcher? = null
for (entry in ExclusiveParserType.entries) { for (entry in ExclusiveParserType.entries) {
val data = jsonElement[entry.key] ?: continue val data = jsonElement[entry.key] ?: continue
if (encounteredParser != null) { if (encounteredParser != null) {
// TODO: warn // TODO: warn
return null return null
} }
encounteredParser = entry.parse(data) ?: return null encounteredParser = entry.parse(data) ?: return null
} }
return encounteredParser return encounteredParser
} }
return null return null
} }
enum class ExclusiveParserType(val key: String) { enum class ExclusiveParserType(val key: String) {
STRING("string") { STRING("string") {
override fun parse(element: JsonElement): NbtMatcher? { override fun parse(element: JsonElement): NbtMatcher? {
return MatchString(StringMatcher.parse(element)) return MatchString(StringMatcher.parse(element))
} }
}, },
INT("int") { INT("int") {
override fun parse(element: JsonElement): NbtMatcher? { override fun parse(element: JsonElement): NbtMatcher? {
return parseGenericNumber(element, return parseGenericNumber(
{ it.asInt }, element,
{ (it as? NbtInt)?.intValue() }, { it.asInt },
{ a, b -> { (it as? NbtInt)?.intValue() },
if (a == b) Comparison.EQUAL { a, b ->
else if (a < b) Comparison.LESS_THAN if (a == b) Comparison.EQUAL
else Comparison.GREATER else if (a < b) Comparison.LESS_THAN
}) else Comparison.GREATER
} })
}, }
FLOAT("float") { },
override fun parse(element: JsonElement): NbtMatcher? { FLOAT("float") {
return parseGenericNumber(element, override fun parse(element: JsonElement): NbtMatcher? {
{ it.asFloat }, return parseGenericNumber(
{ (it as? NbtFloat)?.floatValue() }, element,
{ a, b -> { it.asFloat },
if (a == b) Comparison.EQUAL { (it as? NbtFloat)?.floatValue() },
else if (a < b) Comparison.LESS_THAN { a, b ->
else Comparison.GREATER 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, DOUBLE("double") {
{ it.asDouble }, override fun parse(element: JsonElement): NbtMatcher? {
{ (it as? NbtDouble)?.doubleValue() }, return parseGenericNumber(
{ a, b -> element,
if (a == b) Comparison.EQUAL { it.asDouble },
else if (a < b) Comparison.LESS_THAN { (it as? NbtDouble)?.doubleValue() },
else Comparison.GREATER { 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 }, LONG("long") {
{ (it as? NbtLong)?.longValue() }, override fun parse(element: JsonElement): NbtMatcher? {
{ a, b -> return parseGenericNumber(
if (a == b) Comparison.EQUAL element,
else if (a < b) Comparison.LESS_THAN { it.asLong },
else Comparison.GREATER { (it as? NbtLong)?.longValue() },
}) { a, b ->
} if (a == b) Comparison.EQUAL
}, else if (a < b) Comparison.LESS_THAN
SHORT("short") { else Comparison.GREATER
override fun parse(element: JsonElement): NbtMatcher? { })
return parseGenericNumber(element, }
{ it.asShort }, },
{ (it as? NbtShort)?.shortValue() }, SHORT("short") {
{ a, b -> override fun parse(element: JsonElement): NbtMatcher? {
if (a == b) Comparison.EQUAL return parseGenericNumber(
else if (a < b) Comparison.LESS_THAN element,
else Comparison.GREATER { it.asShort },
}) { (it as? NbtShort)?.shortValue() },
} { a, b ->
}, if (a == b) Comparison.EQUAL
BYTE("byte") { else if (a < b) Comparison.LESS_THAN
override fun parse(element: JsonElement): NbtMatcher? { else Comparison.GREATER
return parseGenericNumber(element, })
{ it.asByte }, }
{ (it as? NbtByte)?.byteValue() }, },
{ a, b -> BYTE("byte") {
if (a == b) Comparison.EQUAL override fun parse(element: JsonElement): NbtMatcher? {
else if (a < b) Comparison.LESS_THAN return parseGenericNumber(
else Comparison.GREATER 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? abstract fun parse(element: JsonElement): NbtMatcher?
} }
enum class Comparison { enum class Comparison {
LESS_THAN, EQUAL, GREATER LESS_THAN, EQUAL, GREATER
} }
inline fun <T : Any> parseGenericNumber( inline fun <T : Any> parseGenericNumber(
jsonElement: JsonElement, jsonElement: JsonElement,
primitiveExtractor: (JsonPrimitive) -> T?, primitiveExtractor: (JsonPrimitive) -> T?,
crossinline nbtExtractor: (NbtElement) -> T?, crossinline nbtExtractor: (NbtElement) -> T?,
crossinline compare: (T, T) -> Comparison crossinline compare: (T, T) -> Comparison
): NbtMatcher? { ): NbtMatcher? {
if (jsonElement is JsonPrimitive) { if (jsonElement is JsonPrimitive) {
val expected = primitiveExtractor(jsonElement) ?: return null val expected = primitiveExtractor(jsonElement) ?: return null
return NbtMatcher { return NbtMatcher {
val actual = nbtExtractor(it) ?: return@NbtMatcher false val actual = nbtExtractor(it) ?: return@NbtMatcher false
compare(actual, expected) == Comparison.EQUAL compare(actual, expected) == Comparison.EQUAL
} }
} }
if (jsonElement is JsonObject) { if (jsonElement is JsonObject) {
val minElement = jsonElement.getAsJsonPrimitive("min") val minElement = jsonElement.getAsJsonPrimitive("min")
val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null
val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false
val maxElement = jsonElement.getAsJsonPrimitive("max") val maxElement = jsonElement.getAsJsonPrimitive("max")
val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null
val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true
if (min == null && max == null) return null if (min == null && max == null) return null
return NbtMatcher { return NbtMatcher {
val actual = nbtExtractor(it) ?: return@NbtMatcher false val actual = nbtExtractor(it) ?: return@NbtMatcher false
if (max != null) { if (max != null) {
val comp = compare(actual, max) val comp = compare(actual, max)
if (comp == Comparison.GREATER) return@NbtMatcher false if (comp == Comparison.GREATER) return@NbtMatcher false
if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false
} }
if (min != null) { if (min != null) {
val comp = compare(actual, min) val comp = compare(actual, min)
if (comp == Comparison.LESS_THAN) return@NbtMatcher false if (comp == Comparison.LESS_THAN) return@NbtMatcher false
if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false
} }
return@NbtMatcher true return@NbtMatcher true
} }
} }
return null return null
} }
} }
class MatchNumberExact(val number: Long) : NbtMatcher { class MatchNumberExact(val number: Long) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean { override fun matches(nbt: NbtElement): Boolean {
return when (nbt) { return when (nbt) {
is NbtByte -> nbt.byteValue().toLong() == number is NbtByte -> nbt.byteValue().toLong() == number
is NbtInt -> nbt.intValue().toLong() == number is NbtInt -> nbt.intValue().toLong() == number
is NbtShort -> nbt.shortValue().toLong() == number is NbtShort -> nbt.shortValue().toLong() == number
is NbtLong -> nbt.longValue().toLong() == number is NbtLong -> nbt.longValue().toLong() == number
else -> false else -> false
} }
} }
} }
class MatchStringExact(val string: String) : NbtMatcher { class MatchStringExact(val string: String) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean { override fun matches(nbt: NbtElement): Boolean {
return nbt is NbtString && nbt.asString() == string return nbt.asString() == string
} }
override fun toString(): String { override fun toString(): String {
return "MatchNbtStringExactly($string)" return "MatchNbtStringExactly($string)"
} }
} }
class MatchString(val string: StringMatcher) : NbtMatcher { class MatchString(val string: StringMatcher) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean { override fun matches(nbt: NbtElement): Boolean {
return nbt is NbtString && string.matches(nbt.asString()) return nbt.asString().let(string::matches)
} }
override fun toString(): String { override fun toString(): String {
return "MatchNbtString($string)" return "MatchNbtString($string)"
} }
} }
} }
data class ExtraAttributesPredicate( data class ExtraAttributesPredicate(
@@ -217,55 +223,63 @@ data class ExtraAttributesPredicate(
val matcher: NbtMatcher, val matcher: NbtMatcher,
) : FirmamentModelPredicate { ) : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser { object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
if (jsonElement !is JsonObject) return null if (jsonElement !is JsonObject) return null
val path = jsonElement.get("path") ?: return null val path = jsonElement.get("path") ?: return null
val pathSegments = if (path is JsonArray) { val prism = NbtPrism.fromElement(path) ?: return null
path.map { (it as JsonPrimitive).asString } val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
} else if (path is JsonPrimitive && path.isString) { ?: return null
path.asString.split(".") return ExtraAttributesPredicate(prism, matcher)
} 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 { override fun test(stack: ItemStack): Boolean {
return path.access(stack.extraAttributes) return path.access(stack.extraAttributes)
.any { matcher.matches(it) } .any { matcher.matches(it) }
} }
} }
class NbtPrism(val path: List<String>) { class NbtPrism(val path: List<String>) {
override fun toString(): String { companion object {
return "Prism($path)" fun fromElement(path: JsonElement): NbtPrism? {
} if (path is JsonArray) {
fun access(root: NbtElement): Collection<NbtElement> { return NbtPrism(path.map { (it as JsonPrimitive).asString })
var rootSet = mutableListOf(root) } else if (path is JsonPrimitive && path.isString) {
var switch = mutableListOf<NbtElement>() return NbtPrism(path.asString.split("."))
for (pathSegment in path) { }
if (pathSegment == ".") continue return null
for (element in rootSet) { }
if (element is NbtList) { }
if (pathSegment == "*")
switch.addAll(element) override fun toString(): String {
val index = pathSegment.toIntOrNull() ?: continue return "Prism($path)"
if (index !in element.indices) continue }
switch.add(element[index])
} fun access(root: NbtElement): Collection<NbtElement> {
if (element is NbtCompound) { var rootSet = mutableListOf(root)
if (pathSegment == "*") var switch = mutableListOf<NbtElement>()
element.keys.mapTo(switch) { element.get(it)!! } for (pathSegment in path) {
switch.add(element.get(pathSegment) ?: continue) if (pathSegment == ".") continue
} for (element in rootSet) {
} if (element is NbtList) {
val temp = switch if (pathSegment == "*")
switch = rootSet switch.addAll(element)
rootSet = temp val index = pathSegment.toIntOrNull() ?: continue
switch.clear() if (index !in element.indices) continue
} switch.add(element[index])
return rootSet }
} 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
}
} }

View File

@@ -0,0 +1,57 @@
package moe.nea.firmament.features.texturepack.predicates
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.mojang.serialization.Codec
import kotlin.jvm.optionals.getOrNull
import net.minecraft.component.ComponentType
import net.minecraft.component.type.NbtComponent
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtOps
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.util.Identifier
import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
import moe.nea.firmament.util.MC
data class GenericComponentPredicate<T>(
val componentType: ComponentType<T>,
val codec: Codec<T>,
val path: NbtPrism,
val matcher: NbtMatcher,
) : FirmamentModelPredicate {
constructor(componentType: ComponentType<T>, path: NbtPrism, matcher: NbtMatcher)
: this(componentType, componentType.codecOrThrow, path, matcher)
override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
val component = stack.get(componentType) ?: return false
// TODO: cache this
val nbt =
if (component is NbtComponent) component.nbt
else codec.encodeStart(NbtOps.INSTANCE, component)
.resultOrPartial().getOrNull() ?: return false
return path.access(nbt).any { matcher.matches(it) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): GenericComponentPredicate<*>? {
if (jsonElement !is JsonObject) return null
val path = jsonElement.get("path") ?: return null
val prism = NbtPrism.fromElement(path) ?: return null
val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
?: return null
val component = MC.currentOrDefaultRegistries
.getOrThrow(RegistryKeys.DATA_COMPONENT_TYPE)
.getOrThrow(
RegistryKey.of(
RegistryKeys.DATA_COMPONENT_TYPE,
Identifier.of(jsonElement.get("component").asString)
)
).value()
return GenericComponentPredicate(component, prism, matcher)
}
}
}

View File

@@ -167,6 +167,32 @@ Sub object match:
} }
``` ```
#### Components
You can match generic components similarly to [extra attributes](#extra-attributes). If you want to match an extra
attribute match directly using that, for better performance.
You can specify a `path` and match similar to extra attributes, but in addition you can also specify a `component`. This
variable is the identifier of a component type that will then be encoded to nbt and matched according to the `match`
using a [nbt matcher](#nbt-matcher).
```json5
"firmament:component": {
"path": "rgb",
"component": "minecraft:dyed_color",
"int": 255
}
// Alternatively
"firmament:component": {
"path": "rgb",
"component": "minecraft:dyed_color",
"match": {
"int": 255
}
}
```
#### Pet Data #### Pet Data
Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to