Add extra attribute item predicate
This commit is contained in:
@@ -86,6 +86,36 @@ Filter by item type:
|
|||||||
"firmament:item": "minecraft:clock"
|
"firmament:item": "minecraft:clock"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Extra attributes
|
||||||
|
|
||||||
|
Filter by extra attribute NBT data:
|
||||||
|
|
||||||
|
Specify a `path` to look at, separating sub elements with a `.`. You can use a `*` to check any child.
|
||||||
|
|
||||||
|
Then either specify a `match` sub-object or directly inline that object in the format of an [nbt matcher](#nbt-matcher).
|
||||||
|
|
||||||
|
Inlined match:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
"firmament:extra_attributes": {
|
||||||
|
"path": "gems.JADE_0",
|
||||||
|
"string": "PERFECT"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Sub object match:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
"firmament:extra_attributes": {
|
||||||
|
"path": "gems.JADE_0",
|
||||||
|
"match": {
|
||||||
|
"string": "PERFECT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Logic Operators
|
#### Logic Operators
|
||||||
|
|
||||||
Logic operators allow to combine other firmament predicates into one. This is done by building boolean operators:
|
Logic operators allow to combine other firmament predicates into one. This is done by building boolean operators:
|
||||||
@@ -151,6 +181,48 @@ specify one of these other matchers and one color preserving property.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Nbt Matcher
|
||||||
|
|
||||||
|
This matches a single nbt element.
|
||||||
|
|
||||||
|
Have the type of the nbt element as json key. Can be `string`, `int`, `float`, `double`, `long`, `short` and `byte`.
|
||||||
|
|
||||||
|
The `string` type matches like a regular [string matcher](#string-matcher):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"string": {
|
||||||
|
"color": "strip",
|
||||||
|
"regex": "^aaa bbb$"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The other (numeric) types can either be matched directly against a number:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"int": 10
|
||||||
|
```
|
||||||
|
|
||||||
|
Or as a range:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"long": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 1000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Min and max are both optional, but you need to specify at least one. By default `min` is inclusive and `max` is exclusive.
|
||||||
|
You can override that like so:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"short": {
|
||||||
|
"min": 0,
|
||||||
|
"max": 1000,
|
||||||
|
"minExclusive": true,
|
||||||
|
"maxExclusive": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Armor textures
|
## Armor textures
|
||||||
|
|
||||||
You can re-*texture* armors, but not re-*model* them with firmament.
|
You can re-*texture* armors, but not re-*model* them with firmament.
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ object CustomModelOverrideParser {
|
|||||||
registerPredicateParser("any", OrPredicate.Parser)
|
registerPredicateParser("any", OrPredicate.Parser)
|
||||||
registerPredicateParser("not", NotPredicate.Parser)
|
registerPredicateParser("not", NotPredicate.Parser)
|
||||||
registerPredicateParser("item", ItemPredicate.Parser)
|
registerPredicateParser("item", ItemPredicate.Parser)
|
||||||
|
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val neverPredicate = listOf(
|
private val neverPredicate = listOf(
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModel
|
|||||||
CustomModelOverrideParser.parsePredicates(it as JsonObject)
|
CustomModelOverrideParser.parsePredicates(it as JsonObject)
|
||||||
}
|
}
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
return AndPredicate(children)
|
return OrPredicate(children)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,13 +46,21 @@ interface StringMatcher {
|
|||||||
override fun matches(string: String): Boolean {
|
override fun matches(string: String): Boolean {
|
||||||
return expected == (if (stripColorCodes) string.removeColorCodes() else string)
|
return expected == (if (stripColorCodes) string.removeColorCodes() else string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Equals($expected, stripColorCodes = $stripColorCodes)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Pattern(patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
|
class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
|
||||||
private val regex: Predicate<String> = patternWithColorCodes.toPattern().asMatchPredicate()
|
private val regex: Predicate<String> = patternWithColorCodes.toPattern().asMatchPredicate()
|
||||||
override fun matches(string: String): Boolean {
|
override fun matches(string: String): Boolean {
|
||||||
return regex.test(if (stripColorCodes) string.removeColorCodes() else string)
|
return regex.test(if (stripColorCodes) string.removeColorCodes() else string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object Serializer : KSerializer<StringMatcher> {
|
object Serializer : KSerializer<StringMatcher> {
|
||||||
@@ -73,7 +81,7 @@ interface StringMatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun serialize(stringMatcher: StringMatcher):JsonElement {
|
fun serialize(stringMatcher: StringMatcher): JsonElement {
|
||||||
TODO("Cannot serialize string matchers rn")
|
TODO("Cannot serialize string matchers rn")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user