Add pet matcher texture pack support

Closes https://github.com/nea89o/Firmament/issues/29
This commit is contained in:
Linnea Gräf
2024-07-06 01:06:53 +02:00
parent dd2c455b19
commit c3d32559e4
5 changed files with 357 additions and 0 deletions

View File

@@ -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 #### 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 ### Nbt Matcher
This matches a single nbt element. 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 ## 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.

View File

@@ -45,6 +45,7 @@ object CustomModelOverrideParser {
registerPredicateParser("not", NotPredicate.Parser) registerPredicateParser("not", NotPredicate.Parser)
registerPredicateParser("item", ItemPredicate.Parser) registerPredicateParser("item", ItemPredicate.Parser)
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
registerPredicateParser("pet", PetPredicate.Parser)
} }
private val neverPredicate = listOf( private val neverPredicate = listOf(

View File

@@ -0,0 +1,130 @@
/*
* 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.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 =
"(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
.toPattern()
fun parseRange(string: String): RangeMatcher? {
intervalSpec.useMatch<Nothing>(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>${Operator.entries.joinToString("|") {it.operator}})(?<value>[0-9.]+)".toPattern()
fun parseOperator(string: String): OperatorMatcher? {
operatorPattern.useMatch<Nothing>(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
}
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.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()
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.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 =
"(?<beginningOpen>[\\[\\(])(?<beginning>$allRarities)?,(?<ending>$allRarities)?(?<endingOpen>[\\]\\)])"
.toPattern()
fun parseRange(string: String): RangeMatcher? {
intervalSpec.useMatch<Nothing>(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
}
}
}