From c3d32559e4132f163509143777ce8c6477a92c70 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Sat, 6 Jul 2024 01:06:53 +0200 Subject: Add pet matcher texture pack support Closes https://github.com/nea89o/Firmament/issues/29 --- docs/Texture Pack Format.md | 81 +++++++++++++ .../texturepack/CustomModelOverrideParser.kt | 1 + .../features/texturepack/NumberMatcher.kt | 130 +++++++++++++++++++++ .../firmament/features/texturepack/PetPredicate.kt | 71 +++++++++++ .../features/texturepack/RarityMatcher.kt | 74 ++++++++++++ 5 files changed, 357 insertions(+) create mode 100644 src/main/kotlin/moe/nea/firmament/features/texturepack/NumberMatcher.kt create mode 100644 src/main/kotlin/moe/nea/firmament/features/texturepack/PetPredicate.kt create mode 100644 src/main/kotlin/moe/nea/firmament/features/texturepack/RarityMatcher.kt diff --git a/docs/Texture Pack Format.md b/docs/Texture Pack Format.md index 54a5d5e..ea54c9d 100644 --- a/docs/Texture Pack Format.md +++ b/docs/Texture Pack Format.md @@ -114,6 +114,30 @@ Sub object match: } ``` +#### Pet Data + +Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to +further filter by level and some other pet info. + +```json5 +"firmament:pet" { + "id": "WOLF", + "exp": ">=25353230", + "tier": "[RARE,LEGENDARY]", + "level": "[50,)", + "candyUsed": 0 +} +``` + +| Name | Type | Description | +|-------------|------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| +| `id` | [String](#string-matcher) | The id of the pet | +| `exp` | [Number](#number-matcher) | The total experience of the pet | +| `tier` | Rarity (like [Number](#number-matcher), but with rarity names instead) | The total experience of the pet | +| `level` | [Number](#number-matcher) | The current level of the pet | +| `candyUsed` | [Number](#number-matcher) | The number of pet candies used on the pet. This is present even if they are not shown in game (such as on a level 100 legendary pet) | + +Every part of this matcher is optional. #### Logic Operators @@ -181,6 +205,58 @@ specify one of these other matchers and one color preserving property. } ``` +### Number Matchers + +This matches a number against either a range or a specific number. + +#### Direct number + +You can directly specify a number using that value directly: +```json5 +"firmament:pet": { + "level": 100 +} +``` + +This is best for whole numbers, since decimal numbers can be really close together but still be different. + +#### Intervals + +For ranges you can instead use an interval. This uses the standard mathematical notation for those as a string: + + +```json5 +"firmament:pet": { + "level": "(50,100]" +} +``` + +This is in the format of `(min,max)` or `[min,max]`. Either min or max can be omitted, which results in that boundary +being ignored (so `[50,)` would be 50 until infinity). You can also vary the parenthesis on either side independently. + +Specifying round parenthesis `()` means the number is exclusive, so not including this number. For example `(50,100)` +would not match just the number `50` or `100`, but would match `51`. + +Specifying square brackets `[]` means the number is inclusive. For example `[50,100]` would match both `50` and `100`. + +You can mix and match parenthesis and brackets, they only ever affect the number next to it. + +For more information in intervals check out [Wikipedia](https://en.wikipedia.org/wiki/Interval_(mathematics)). + +#### Operators + +If instead of specifying a range you just need to specify one boundary you can also use the standard operators to +compare your number: + +```json5 +"firmament:pet": { + "level": "<50" +} +``` + +This example would match if the level is less than fifty. The available operators are `<`, `>`, `<=` and `>=`. The +operator needs to be specified on the left. The versions of the operator with `=` also allow the number to be equal. + ### Nbt Matcher This matches a single nbt element. @@ -223,6 +299,11 @@ You can override that like so: } ``` + +> [!WARNING] +> This syntax for numbers is *just* for **NBT values**. This is also why specifying the type of the number is necessary. +> For other number matchers, use [the number matchers](#number-matchers) + ## 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 4ae73e8..5c16150 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -45,6 +45,7 @@ object CustomModelOverrideParser { registerPredicateParser("not", NotPredicate.Parser) registerPredicateParser("item", ItemPredicate.Parser) registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) + registerPredicateParser("pet", PetPredicate.Parser) } private val neverPredicate = listOf( diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/NumberMatcher.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/NumberMatcher.kt new file mode 100644 index 0000000..b39330e --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/NumberMatcher.kt @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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 = + "(?[\\[\\(])(?[0-9.]+)?,(?[0-9.]+)?(?[\\]\\)])" + .toPattern() + + fun parseRange(string: String): RangeMatcher? { + intervalSpec.useMatch(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.entries.joinToString("|") {it.operator}})(?[0-9.]+)".toPattern() + + fun parseOperator(string: String): OperatorMatcher? { + operatorPattern.useMatch(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/moe/nea/firmament/features/texturepack/PetPredicate.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/PetPredicate.kt new file mode 100644 index 0000000..b252832 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/PetPredicate.kt @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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/moe/nea/firmament/features/texturepack/RarityMatcher.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/RarityMatcher.kt new file mode 100644 index 0000000..1a0daac --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/RarityMatcher.kt @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +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 = + "(?[\\[\\(])(?$allRarities)?,(?$allRarities)?(?[\\]\\)])" + .toPattern() + + fun parseRange(string: String): RangeMatcher? { + intervalSpec.useMatch(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 + } + } + +} -- cgit