aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2024-05-14 23:59:03 +0200
committerLinnea Gräf <nea@nea.moe>2024-05-15 00:19:40 +0200
commitd7d863f76d78b90179918222539fe4de05f7b44e (patch)
tree9481ba5d75e685f4f599428388c668a277e60b18
parentfd069385118406cd641c802e0a3a7d166a482eee (diff)
downloadFirmament-d7d863f76d78b90179918222539fe4de05f7b44e.tar.gz
Firmament-d7d863f76d78b90179918222539fe4de05f7b44e.tar.bz2
Firmament-d7d863f76d78b90179918222539fe4de05f7b44e.zip
Add extra attribute item predicate
-rw-r--r--docs/Texture Pack Format.md72
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt1
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/texturepack/ExtraAttributesPredicate.kt273
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/texturepack/OrPredicate.kt2
-rw-r--r--src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt12
5 files changed, 357 insertions, 3 deletions
diff --git a/docs/Texture Pack Format.md b/docs/Texture Pack Format.md
index 1aedde9..914e157 100644
--- a/docs/Texture Pack Format.md
+++ b/docs/Texture Pack Format.md
@@ -86,6 +86,36 @@ Filter by item type:
"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 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
You can re-*texture* armors, but not re-*model* them with firmament.
diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
index 2f5667b..458f8ff 100644
--- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
@@ -44,6 +44,7 @@ object CustomModelOverrideParser {
registerPredicateParser("any", OrPredicate.Parser)
registerPredicateParser("not", NotPredicate.Parser)
registerPredicateParser("item", ItemPredicate.Parser)
+ registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
}
private val neverPredicate = listOf(
diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/ExtraAttributesPredicate.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/ExtraAttributesPredicate.kt
new file mode 100644
index 0000000..4a726cc
--- /dev/null
+++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/ExtraAttributesPredicate.kt
@@ -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
+ }
+}
diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/OrPredicate.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/OrPredicate.kt
index c171367..cab918e 100644
--- a/src/main/kotlin/moe/nea/firmament/features/texturepack/OrPredicate.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/OrPredicate.kt
@@ -24,7 +24,7 @@ class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModel
CustomModelOverrideParser.parsePredicates(it as JsonObject)
}
.toTypedArray()
- return AndPredicate(children)
+ return OrPredicate(children)
}
}
diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt
index 1c2675f..3fc3ab9 100644
--- a/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt
+++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt
@@ -46,13 +46,21 @@ interface StringMatcher {
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(patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
+ 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> {
@@ -73,7 +81,7 @@ interface StringMatcher {
}
companion object {
- fun serialize(stringMatcher: StringMatcher):JsonElement {
+ fun serialize(stringMatcher: StringMatcher): JsonElement {
TODO("Cannot serialize string matchers rn")
}