Refactor source layout

Introduce compat source sets and move all kotlin sources to the main directory

[no changelog]
This commit is contained in:
Linnea Gräf
2024-08-28 19:04:24 +02:00
parent a690630816
commit d2f240ff0c
251 changed files with 295 additions and 38 deletions

View File

@@ -0,0 +1,17 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
object AlwaysPredicate : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return true
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return AlwaysPredicate
}
}
}

View File

@@ -0,0 +1,26 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.all { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
val children =
(jsonElement as JsonArray)
.flatMap {
CustomModelOverrideParser.parsePredicates(it as JsonObject)
}
.toTypedArray()
return AndPredicate(children)
}
}
}

View File

@@ -0,0 +1,9 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.client.render.model.BakedModel
interface BakedModelExtra {
fun getHeadModel_firmament(): BakedModel?
fun setHeadModel_firmament(headModel: BakedModel?)
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
interface BakedOverrideData {
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
}

View File

@@ -0,0 +1,295 @@
@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class)
package moe.nea.firmament.features.texturepack
import java.util.concurrent.CompletableFuture
import net.fabricmc.loader.api.FabricLoader
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.serializer
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.EarlyResourceReloadEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.json.BlockPosSerializer
import moe.nea.firmament.util.json.SingletonSerializableList
object CustomBlockTextures {
@Serializable
data class CustomBlockOverride(
val modes: @Serializable(SingletonSerializableList::class) List<String>,
val area: List<Area>? = null,
val replacements: Map<Identifier, Replacement>,
)
@Serializable(with = Replacement.Serializer::class)
data class Replacement(
val block: Identifier,
val sound: Identifier?,
) {
@Transient
val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament")
@Transient
val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) {
MC.instance.bakedModelManager.getModel(blockModelIdentifier)
}
@OptIn(ExperimentalSerializationApi::class)
@kotlinx.serialization.Serializer(Replacement::class)
object DefaultSerializer : KSerializer<Replacement>
object Serializer : KSerializer<Replacement> {
val delegate = serializer<JsonElement>()
override val descriptor: SerialDescriptor
get() = delegate.descriptor
override fun deserialize(decoder: Decoder): Replacement {
val jsonElement = decoder.decodeSerializableValue(delegate)
if (jsonElement is JsonPrimitive) {
require(jsonElement.isString)
return Replacement(Identifier.tryParse(jsonElement.content)!!, null)
}
return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement)
}
override fun serialize(encoder: Encoder, value: Replacement) {
encoder.encodeSerializableValue(DefaultSerializer, value)
}
}
}
@Serializable
data class Area(
val min: BlockPos,
val max: BlockPos,
) {
@Transient
val realMin = BlockPos(
minOf(min.x, max.x),
minOf(min.y, max.y),
minOf(min.z, max.z),
)
@Transient
val realMax = BlockPos(
maxOf(min.x, max.x),
maxOf(min.y, max.y),
maxOf(min.z, max.z),
)
fun roughJoin(other: Area): Area {
return Area(
BlockPos(
minOf(realMin.x, other.realMin.x),
minOf(realMin.y, other.realMin.y),
minOf(realMin.z, other.realMin.z),
),
BlockPos(
maxOf(realMax.x, other.realMax.x),
maxOf(realMax.y, other.realMax.y),
maxOf(realMax.z, other.realMax.z),
)
)
}
fun contains(blockPos: BlockPos): Boolean {
return (blockPos.x in realMin.x..realMax.x) &&
(blockPos.y in realMin.y..realMax.y) &&
(blockPos.z in realMin.z..realMax.z)
}
}
data class LocationReplacements(
val lookup: Map<Block, List<BlockReplacement>>
)
data class BlockReplacement(
val checks: List<Area>?,
val replacement: Replacement,
) {
val roughCheck by lazy(LazyThreadSafetyMode.NONE) {
if (checks == null || checks.size < 3) return@lazy null
checks.reduce { acc, next -> acc.roughJoin(next) }
}
}
data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>)
var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf())
var currentIslandReplacements: LocationReplacements? = null
fun refreshReplacements() {
val location = SBData.skyblockLocation
val replacements =
if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get)
else null
val lastReplacements = currentIslandReplacements
currentIslandReplacements = replacements
if (lastReplacements != replacements) {
MC.nextTick {
MC.worldRenderer.chunks?.chunks?.forEach {
// false schedules rebuilds outside a 27 block radius to happen async
it.scheduleRebuild(false)
}
sodiumReloadTask?.run()
}
}
}
private val sodiumReloadTask = runCatching {
Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader").getConstructor().newInstance() as Runnable
}.getOrElse {
if (FabricLoader.getInstance().isModLoaded("sodium"))
logger.error("Could not create sodium chunk reloader")
null
}
fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean {
if (blockPos == null) return true
val rc = replacement.roughCheck
if (rc != null && !rc.contains(blockPos)) return false
val areas = replacement.checks
if (areas != null && !areas.any { it.contains(blockPos) }) return false
return true
}
@JvmStatic
fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BakedModel? {
return getReplacement(block, blockPos)?.bakedModel
}
@JvmStatic
fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? {
if (isInFallback() && blockPos == null) return null
val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null
for (replacement in replacements) {
if (replacement.checks == null || matchesPosition(replacement, blockPos))
return replacement.replacement
}
return null
}
@Subscribe
fun onLocation(event: SkyblockServerUpdateEvent) {
refreshReplacements()
}
@Volatile
var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
mapOf()))
val insideFallbackCall = ThreadLocal.withInitial { 0 }
@JvmStatic
fun enterFallbackCall() {
insideFallbackCall.set(insideFallbackCall.get() + 1)
}
fun isInFallback() = insideFallbackCall.get() > 0
@JvmStatic
fun exitFallbackCall() {
insideFallbackCall.set(insideFallbackCall.get() - 1)
}
@Subscribe
fun onEarlyReload(event: EarlyResourceReloadEvent) {
preparationFuture = CompletableFuture
.supplyAsync(
{ prepare(event.resourceManager) }, event.preparationExecutor)
}
@Subscribe
fun bakeExtraModels(event: BakeExtraModelsEvent) {
preparationFuture.join().data.values
.flatMap { it.lookup.values }
.flatten()
.mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier }
.forEach { event.addNonItemModel(it) }
}
private fun prepare(manager: ResourceManager): BakedReplacements {
val resources = manager.findResources("overrides/blocks") {
it.namespace == "firmskyblock" && it.path.endsWith(".json")
}
val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>()
for ((file, resource) in resources) {
val json =
Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream)
.getOrElse { ex ->
logger.error("Failed to load block texture override at $file", ex)
continue
}
for (mode in json.modes) {
val island = SkyBlockIsland.forMode(mode)
val islandMpa = map.getOrPut(island, ::mutableMapOf)
for ((blockId, replacement) in json.replacements) {
val block = MC.defaultRegistries.getWrapperOrThrow(RegistryKeys.BLOCK)
.getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId))
.getOrNull()
if (block == null) {
logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'")
continue
}
val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf)
replacements.add(BlockReplacement(json.area, replacement))
}
}
}
return BakedReplacements(map.mapValues { LocationReplacements(it.value) })
}
@JvmStatic
fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel {
return getReplacementModel(state, pos) ?: orig
}
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(object :
SinglePreparationResourceReloader<BakedReplacements>() {
override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
return preparationFuture.join()
}
override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) {
allLocationReplacements = prepared
refreshReplacements()
}
})
}
}

View File

@@ -0,0 +1,106 @@
@file:UseSerializers(IdentifierSerializer::class)
package moe.nea.firmament.features.texturepack
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.UseSerializers
import net.minecraft.item.ArmorMaterial
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.computeNullableFunction
import moe.nea.firmament.util.skyBlockId
object CustomGlobalArmorOverrides : SubscriptionOwner {
@Serializable
data class ArmorOverride(
@SerialName("item_ids")
val itemIds: List<String>,
val layers: List<ArmorOverrideLayer>,
val overrides: List<ArmorOverrideOverride> = listOf(),
) {
@Transient
val bakedLayers = bakeLayers(layers)
}
fun bakeLayers(layers: List<ArmorOverrideLayer>): List<ArmorMaterial.Layer> {
return layers.map { ArmorMaterial.Layer(it.identifier, it.suffix, it.tint) }
}
@Serializable
data class ArmorOverrideLayer(
val tint: Boolean = false,
val identifier: Identifier,
val suffix: String = "",
)
@Serializable
data class ArmorOverrideOverride(
val predicate: FirmamentModelPredicate,
val layers: List<ArmorOverrideLayer>,
) {
@Transient
val bakedLayers = bakeLayers(layers)
}
override val delegateFeature: FirmamentFeature
get() = CustomSkyBlockTextures
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
@JvmStatic
fun overrideArmor(stack: ItemStack): List<ArmorMaterial.Layer>? {
if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null
return overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
val id = stack.skyBlockId ?: return@computeNullableFunction null
val override = overrides[id.neuItem] ?: return@computeNullableFunction null
for (suboverride in override.overrides) {
if (suboverride.predicate.test(stack)) {
return@computeNullableFunction suboverride.bakedLayers
}
}
return@computeNullableFunction override.bakedLayers
}
}
var overrides: Map<String, ArmorOverride> = mapOf()
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(object :
SinglePreparationResourceReloader<Map<String, ArmorOverride>>() {
override fun prepare(manager: ResourceManager, profiler: Profiler): Map<String, ArmorOverride> {
val overrideFiles = manager.findResources("overrides/armor_models") {
it.namespace == "firmskyblock" && it.path.endsWith(".json")
}
val overrides = overrideFiles.mapNotNull {
Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex ->
logger.error("Failed to load armor texture override at ${it.key}", ex)
null
}
}
val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } }
.toMap()
return associatedMap
}
override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
overrides = prepared
}
})
}
}

View File

@@ -0,0 +1,167 @@
@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class)
package moe.nea.firmament.features.texturepack
import java.util.concurrent.CompletableFuture
import org.slf4j.LoggerFactory
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.render.item.ItemModels
import net.minecraft.client.render.model.BakedModel
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.item.ItemStack
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.profiler.Profiler
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.EarlyResourceReloadEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.subscription.SubscriptionOwner
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.computeNullableFunction
import moe.nea.firmament.util.json.SingletonSerializableList
import moe.nea.firmament.util.runNull
object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalTextures.CustomGuiTextureOverride>(),
SubscriptionOwner {
override val delegateFeature: FirmamentFeature
get() = CustomSkyBlockTextures
class CustomGuiTextureOverride(
val classes: List<ItemOverrideCollection>
)
@Serializable
data class GlobalItemOverride(
val screen: @Serializable(SingletonSerializableList::class) List<Identifier>,
val model: Identifier,
val predicate: FirmamentModelPredicate,
)
@Serializable
data class ScreenFilter(
val title: StringMatcher,
)
data class ItemOverrideCollection(
val screenFilter: ScreenFilter,
val overrides: List<GlobalItemOverride>,
)
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
MC.resourceManager.registerReloader(this)
}
@Subscribe
fun onEarlyReload(event: EarlyResourceReloadEvent) {
preparationFuture = CompletableFuture
.supplyAsync(
{
prepare(event.resourceManager)
}, event.preparationExecutor)
}
@Subscribe
fun onBakeModels(event: BakeExtraModelsEvent) {
for (guiClassOverride in preparationFuture.join().classes) {
for (override in guiClassOverride.overrides) {
event.addItemModel(ModelIdentifier(override.model, "inventory"))
}
}
}
@Volatile
var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture(
CustomGuiTextureOverride(listOf()))
override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride {
return preparationFuture.join()
}
override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) {
this.guiClassOverrides = prepared
}
val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java)
fun prepare(manager: ResourceManager): CustomGuiTextureOverride {
val overrideResources =
manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") }
.mapNotNull {
Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex ->
logger.error("Failed to load global item override at ${it.key}", ex)
null
}
}
val byGuiClass = overrideResources.flatMap { override -> override.screen.toSet().map { it to override } }
.groupBy { it.first }
val guiClasses = byGuiClass.entries
.mapNotNull {
val key = it.key
val guiClassResource =
manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json"))
.getOrNull()
?: return@mapNotNull runNull {
logger.error("Failed to locate screen filter at $key")
}
val screenFilter =
Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
.getOrElse { ex ->
logger.error("Failed to load screen filter at $key", ex)
return@mapNotNull null
}
ItemOverrideCollection(screenFilter, it.value.map { it.second })
}
logger.info("Loaded ${overrideResources.size} global item overrides")
return CustomGuiTextureOverride(guiClasses)
}
var guiClassOverrides = CustomGuiTextureOverride(listOf())
var matchingOverrides: Set<ItemOverrideCollection> = setOf()
@Subscribe
fun onOpenGui(event: ScreenChangeEvent) {
val newTitle = event.new?.title ?: Text.empty()
matchingOverrides = guiClassOverrides.classes
.filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) }
}
val overrideCache = mutableMapOf<IdentityCharacteristics<ItemStack>, Any>()
@JvmStatic
fun replaceGlobalModel(
models: ItemModels,
stack: ItemStack,
cir: CallbackInfoReturnable<BakedModel>
) {
val value = overrideCache.computeNullableFunction(IdentityCharacteristics(stack)) {
for (guiClassOverride in matchingOverrides) {
for (override in guiClassOverride.overrides) {
if (override.predicate.test(stack)) {
return@computeNullableFunction models.modelManager.getModel(
ModelIdentifier(override.model, "inventory"))
}
}
}
null
}
if (value != null)
cir.returnValue = value
}
}

View File

@@ -0,0 +1,74 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonObject
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
import net.minecraft.util.Identifier
object CustomModelOverrideParser {
object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> {
val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer()
override val descriptor: SerialDescriptor
get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor)
override fun deserialize(decoder: Decoder): FirmamentModelPredicate {
val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject
return AndPredicate(parsePredicates(json).toTypedArray())
}
override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) {
TODO("Cannot serialize firmament predicates")
}
}
val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>()
fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) {
predicateParsers[Identifier.of("firmament", name)] = parser
}
init {
registerPredicateParser("display_name", DisplayNamePredicate.Parser)
registerPredicateParser("lore", LorePredicate.Parser)
registerPredicateParser("all", AndPredicate.Parser)
registerPredicateParser("any", OrPredicate.Parser)
registerPredicateParser("not", NotPredicate.Parser)
registerPredicateParser("item", ItemPredicate.Parser)
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
registerPredicateParser("pet", PetPredicate.Parser)
}
private val neverPredicate = listOf(
object : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return false
}
}
)
fun parsePredicates(predicates: JsonObject): List<FirmamentModelPredicate> {
val parsedPredicates = mutableListOf<FirmamentModelPredicate>()
for (predicateName in predicates.keySet()) {
if (!predicateName.startsWith("firmament:")) continue
val identifier = Identifier.of(predicateName)
val parser = predicateParsers[identifier] ?: return neverPredicate
val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate
parsedPredicates.add(parsedPredicate)
}
return parsedPredicates
}
@JvmStatic
fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? {
val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null
val parsedPredicates = parsePredicates(predicates)
if (parsedPredicates.isEmpty())
return null
return parsedPredicates.toTypedArray()
}
}

View File

@@ -0,0 +1,114 @@
package moe.nea.firmament.features.texturepack
import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.block.SkullBlock
import net.minecraft.client.MinecraftClient
import net.minecraft.client.render.RenderLayer
import net.minecraft.client.util.ModelIdentifier
import net.minecraft.component.type.ProfileComponent
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.BakeExtraModelsEvent
import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.IdentityCharacteristics
import moe.nea.firmament.util.item.decodeProfileTextureProperty
import moe.nea.firmament.util.skyBlockId
object CustomSkyBlockTextures : FirmamentFeature {
override val identifier: String
get() = "custom-skyblock-textures"
object TConfig : ManagedConfig(identifier) {
val enabled by toggle("enabled") { true }
val skullsEnabled by toggle("skulls-enabled") { true }
val cacheDuration by integer("cache-duration", 0, 20) { 1 }
val enableModelOverrides by toggle("model-overrides") { true }
val enableArmorOverrides by toggle("armor-overrides") { true }
val enableBlockOverrides by toggle("block-overrides") { true }
}
override val config: ManagedConfig
get() = TConfig
@Subscribe
fun onTick(it: TickEvent) {
if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
// TODO: unify all of those caches somehow
CustomItemModelEvent.clearCache()
skullTextureCache.clear()
CustomGlobalTextures.overrideCache.clear()
CustomGlobalArmorOverrides.overrideCache.clear()
}
}
@Subscribe
fun bakeCustomFirmModels(event: BakeExtraModelsEvent) {
val resources =
MinecraftClient.getInstance().resourceManager.findResources("models/item"
) { it: Identifier ->
"firmskyblock" == it.namespace && it.path
.endsWith(".json")
}
for (identifier in resources.keys) {
val modelId = ModelIdentifier.ofInventoryVariant(
Identifier.of(
"firmskyblock",
identifier.path.substring(
"models/item/".length,
identifier.path.length - ".json".length),
))
event.addItemModel(modelId)
}
}
@Subscribe
fun onCustomModelId(it: CustomItemModelEvent) {
if (!TConfig.enabled) return
val id = it.itemStack.skyBlockId ?: return
it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path))
}
private val skullTextureCache = mutableMapOf<IdentityCharacteristics<ProfileComponent>, Any>()
private val sentinelPresentInvalid = Object()
private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex()
fun getSkullId(textureProperty: Property): String? {
val texture = decodeProfileTextureProperty(textureProperty) ?: return null
val textureUrl =
texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null
val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null
return mcUrlData.groupValues[1]
}
fun getSkullTexture(profile: ProfileComponent): Identifier? {
val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null
return Identifier.of("firmskyblock", "textures/placedskull/$id.png")
}
fun modifySkullTexture(
type: SkullBlock.SkullType?,
component: ProfileComponent?,
cir: CallbackInfoReturnable<RenderLayer>
) {
if (type != SkullBlock.Type.PLAYER) return
if (!TConfig.skullsEnabled) return
if (component == null) return
val ic = IdentityCharacteristics(component)
val n = skullTextureCache.getOrPut(ic) {
val id = getSkullTexture(component) ?: return@getOrPut sentinelPresentInvalid
if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
return@getOrPut sentinelPresentInvalid
}
return@getOrPut id
}
if (n === sentinelPresentInvalid) return
cir.returnValue = RenderLayer.getEntityTranslucent(n as Identifier)
}
}

View File

@@ -0,0 +1,22 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtString
import moe.nea.firmament.util.item.displayNameAccordingToNbt
import moe.nea.firmament.util.item.loreAccordingToNbt
data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
val display = stack.displayNameAccordingToNbt
return stringMatcher.matches(display)
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return DisplayNamePredicate(StringMatcher.parse(jsonElement))
}
}
}

View File

@@ -0,0 +1,268 @@
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
}
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.item.ItemStack
interface FirmamentModelPredicate {
fun test(stack: ItemStack): Boolean
}

View File

@@ -0,0 +1,8 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
interface FirmamentModelPredicateParser {
fun parse(jsonElement: JsonElement): FirmamentModelPredicate?
}

View File

@@ -0,0 +1,32 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.util.Identifier
import moe.nea.firmament.util.MC
class ItemPredicate(
val item: Item
) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return stack.item == item
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): ItemPredicate? {
if (jsonElement is JsonPrimitive && jsonElement.isString) {
val itemKey = RegistryKey.of(RegistryKeys.ITEM,
Identifier.tryParse(jsonElement.asString)
?: return null)
return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null)
}
return null
}
}
}

View File

@@ -0,0 +1,10 @@
package moe.nea.firmament.features.texturepack
import net.minecraft.util.Identifier
interface JsonUnbakedModelFirmExtra {
fun setHeadModel_firmament(identifier: Identifier?)
fun getHeadModel_firmament(): Identifier?
}

View File

@@ -0,0 +1,19 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import net.minecraft.item.ItemStack
import moe.nea.firmament.util.item.loreAccordingToNbt
class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return LorePredicate(StringMatcher.parse(jsonElement))
}
}
override fun test(stack: ItemStack): Boolean {
val lore = stack.loreAccordingToNbt
return lore.any { matcher.matches(it) }
}
}

View File

@@ -0,0 +1,7 @@
package moe.nea.firmament.features.texturepack
interface ModelOverrideData {
fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
}

View File

@@ -0,0 +1,19 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import moe.nea.firmament.util.filter.IteratorFilterSet
class ModelOverrideFilterSet(original: java.util.Set<Map.Entry<String, JsonElement>>) :
IteratorFilterSet<Map.Entry<String, JsonElement>>(original) {
companion object {
@JvmStatic
fun createFilterSet(set: java.util.Set<*>): java.util.Set<*> {
return ModelOverrideFilterSet(set as java.util.Set<Map.Entry<String, JsonElement>>) as java.util.Set<*>
}
}
override fun shouldKeepElement(element: Map.Entry<String, JsonElement>): Boolean {
return !element.key.startsWith("firmament:")
}
}

View File

@@ -0,0 +1,18 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.none { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
return NotPredicate(CustomModelOverrideParser.parsePredicates(jsonElement as JsonObject).toTypedArray())
}
}
}

View File

@@ -0,0 +1,125 @@
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,26 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import net.minecraft.item.ItemStack
class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
return children.any { it.test(stack) }
}
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
val children =
(jsonElement as JsonArray)
.flatMap {
CustomModelOverrideParser.parsePredicates(it as JsonObject)
}
.toTypedArray()
return OrPredicate(children)
}
}
}

View File

@@ -0,0 +1,66 @@
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,69 @@
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
}
}
}

View File

@@ -0,0 +1,159 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonNull
import com.google.gson.JsonObject
import com.google.gson.JsonPrimitive
import com.google.gson.internal.LazilyParsedNumber
import java.util.function.Predicate
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import net.minecraft.nbt.NbtString
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.removeColorCodes
@Serializable(with = StringMatcher.Serializer::class)
interface StringMatcher {
fun matches(string: String): Boolean
fun matches(text: Text): Boolean {
return matches(text.string)
}
fun matches(nbt: NbtString): Boolean {
val string = nbt.asString()
val jsonStart = string.indexOf('{')
val stringStart = string.indexOf('"')
val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank()
val isJson = jsonStart >= 0 && string.subSequence(0, jsonStart).isBlank()
if (isString || isJson)
return matches(Text.Serialization.fromJson(string, MC.defaultRegistries) ?: return false)
return matches(string)
}
class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher {
private val expected = if (stripColorCodes) input.removeColorCodes() else input
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(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> {
val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer()
override val descriptor: SerialDescriptor
get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor)
override fun deserialize(decoder: Decoder): StringMatcher {
val delegate = decoder.decodeSerializableValue(delegateSerializer)
val gsonDelegate = delegate.intoGson()
return parse(gsonDelegate)
}
override fun serialize(encoder: Encoder, value: StringMatcher) {
encoder.encodeSerializableValue(delegateSerializer, Companion.serialize(value).intoKotlinJson())
}
}
companion object {
fun serialize(stringMatcher: StringMatcher): JsonElement {
TODO("Cannot serialize string matchers rn")
}
fun parse(jsonElement: JsonElement): StringMatcher {
if (jsonElement is JsonPrimitive) {
return Equals(jsonElement.asString, true)
}
if (jsonElement is JsonObject) {
val regex = jsonElement["regex"] as JsonPrimitive?
val text = jsonElement["equals"] as JsonPrimitive?
val shouldStripColor = when (val color = (jsonElement["color"] as JsonPrimitive?)?.asString) {
"preserve" -> false
"strip", null -> true
else -> error("Unknown color preservation mode: $color")
}
if ((regex == null) == (text == null)) error("Could not parse $jsonElement as string matcher")
if (regex != null)
return Pattern(regex.asString, shouldStripColor)
if (text != null)
return Equals(text.asString, shouldStripColor)
}
error("Could not parse $jsonElement as a string matcher")
}
}
}
fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement {
when (this) {
is JsonNull -> return kotlinx.serialization.json.JsonNull
is JsonObject -> {
return kotlinx.serialization.json.JsonObject(this.entrySet()
.associate { it.key to it.value.intoKotlinJson() })
}
is JsonArray -> {
return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() })
}
is JsonPrimitive -> {
if (this.isString)
return kotlinx.serialization.json.JsonPrimitive(this.asString)
if (this.isBoolean)
return kotlinx.serialization.json.JsonPrimitive(this.asBoolean)
return kotlinx.serialization.json.JsonPrimitive(this.asNumber)
}
else -> error("Unknown json variant $this")
}
}
fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement {
when (this) {
is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE
is kotlinx.serialization.json.JsonPrimitive -> {
if (this.isString)
return JsonPrimitive(this.content)
if (this.content == "true")
return JsonPrimitive(true)
if (this.content == "false")
return JsonPrimitive(false)
return JsonPrimitive(LazilyParsedNumber(this.content))
}
is kotlinx.serialization.json.JsonObject -> {
val obj = JsonObject()
for ((k, v) in this) {
obj.add(k, v.intoGson())
}
return obj
}
is kotlinx.serialization.json.JsonArray -> {
val arr = JsonArray()
for (v in this) {
arr.add(v.intoGson())
}
return arr
}
}
}