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

View File

@@ -22,194 +22,200 @@ import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.extraAttributes
fun interface NbtMatcher {
fun matches(nbt: NbtElement): Boolean
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
}
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
})
}
},
;
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?
}
abstract fun parse(element: JsonElement): NbtMatcher?
}
enum class Comparison {
LESS_THAN, EQUAL, GREATER
}
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
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 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
}
class MatchStringExact(val string: String) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean {
return nbt.asString() == string
}
override fun toString(): String {
return "MatchNbtStringExactly($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())
}
class MatchString(val string: StringMatcher) : NbtMatcher {
override fun matches(nbt: NbtElement): Boolean {
return nbt.asString().let(string::matches)
}
override fun toString(): String {
return "MatchNbtString($string)"
}
}
override fun toString(): String {
return "MatchNbtString($string)"
}
}
}
data class ExtraAttributesPredicate(
@@ -217,55 +223,63 @@ data class ExtraAttributesPredicate(
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)
}
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
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
return ExtraAttributesPredicate(prism, matcher)
}
}
override fun test(stack: ItemStack): Boolean {
return path.access(stack.extraAttributes)
.any { matcher.matches(it) }
}
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
}
companion object {
fun fromElement(path: JsonElement): NbtPrism? {
if (path is JsonArray) {
return NbtPrism(path.map { (it as JsonPrimitive).asString })
} else if (path is JsonPrimitive && path.isString) {
return NbtPrism(path.asString.split("."))
}
return null
}
}
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
}
}

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
Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to