diff options
Diffstat (limited to 'src/texturePacks/java/moe/nea/firmament/features')
29 files changed, 1790 insertions, 832 deletions
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt new file mode 100644 index 0000000..d95712b --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt @@ -0,0 +1,11 @@ +package moe.nea.firmament.features.texturepack + +import moe.nea.firmament.util.compatloader.CompatMeta +import moe.nea.firmament.util.compatloader.ICompatMeta + +@CompatMeta +object Compat : ICompatMeta { + override fun shouldLoad(): Boolean { + return true + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt index dc3b109..bc0f36a 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -2,7 +2,12 @@ package moe.nea.firmament.features.texturepack +import com.google.gson.JsonParseException +import com.google.gson.JsonParser +import com.mojang.serialization.JsonOps import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executor +import java.util.function.Function import net.fabricmc.loader.api.FabricLoader import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer @@ -17,23 +22,37 @@ 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 net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.Blocks +import net.minecraft.client.resources.model.ModelBaker +import net.minecraft.client.renderer.block.model.BlockStateModel +import net.minecraft.client.resources.model.BlockStateModelLoader +import net.minecraft.client.resources.model.ModelDiscovery +import net.minecraft.client.renderer.block.model.SingleVariant +import net.minecraft.client.renderer.block.model.BlockModelDefinition +import net.minecraft.client.renderer.block.model.Variant +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.server.packs.resources.Resource +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.world.level.block.state.StateDefinition +import net.minecraft.resources.ResourceLocation +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.AABB +import net.minecraft.util.profiling.ProfilerFiller +import net.minecraft.util.thread.ParallelMapTransform import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.EarlyResourceReloadEvent import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.features.debug.DebugLogger +import moe.nea.firmament.features.texturepack.CustomBlockTextures.createBakedModels import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger +import moe.nea.firmament.util.ErrorUtil import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.MC import moe.nea.firmament.util.SBData @@ -43,249 +62,406 @@ 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( + @Serializable + data class CustomBlockOverride( + val modes: @Serializable(SingletonSerializableList::class) List<String>, + val area: List<Area>? = null, + val replacements: Map<ResourceLocation, Replacement>, + ) + + @Serializable(with = Replacement.Serializer::class) + data class Replacement( + val block: ResourceLocation, + val sound: ResourceLocation?, + ) { + fun replace(block: BlockState): BlockStateModel? { + blockStateMap?.let { return it[block] } + return blockModel + } + + @Transient + lateinit var overridingBlock: Block + + @Transient + val blockModelIdentifier get() = block.withPrefix("block/") + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete, if [unbakedBlockStateMap] is set. + */ + @Transient + var blockStateMap: Map<BlockState, BlockStateModel>? = null + + @Transient + var unbakedBlockStateMap: Map<BlockState, BlockStateModel.UnbakedRoot>? = null + + /** + * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete. Prefer [blockStateMap] if present. + */ + @Transient + lateinit var blockModel: BlockStateModel + + @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(ResourceLocation.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 { - val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader") - .getConstructor() - .newInstance() as Runnable - r.run() - r - }.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) - } - - 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.getOrThrow(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() - } - }) - } + ) { + @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) + } + + fun toBox(): AABB { + return AABB( + realMin.x.toDouble(), + realMin.y.toDouble(), + realMin.z.toDouble(), + (realMax.x + 1).toDouble(), + (realMax.y + 1).toDouble(), + (realMax.z + 1).toDouble() + ) + } + } + + data class LocationReplacements( + val lookup: Map<Block, List<BlockReplacement>> + ) { + init { + lookup.forEach { (block, replacements) -> + for (replacement in replacements) { + replacement.replacement.overridingBlock = block + } + } + } + } + + 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>) { + /** + * Fulfilled by [createBakedModels] which is called during model baking. Once completed, all [Replacement.blockModel] will be set. + */ + val modelBakingFuture = CompletableFuture<Unit>() + + /** + * @returns a list of all [Replacement]s. + */ + fun collectAllReplacements(): Sequence<Replacement> { + return data.values.asSequence() + .flatMap { it.lookup.values } + .flatten() + .map { it.replacement } + } + } + + 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.viewArea?.sections?.forEach { + // false schedules rebuilds outside a 27 block radius to happen async + // nb: this sets the dirty but to true, the boolean parameter specifies the update behaviour + it.setDirty(false) + } + sodiumReloadTask?.run() + } + } + } + + private val sodiumReloadTask = runCatching { + val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader") + .getConstructor() + .newInstance() as Runnable + r.run() + r + }.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?): BlockStateModel? { + return getReplacement(block, blockPos)?.replace(block) + } + + @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 + @get:JvmStatic + 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 + ) + } + + private fun prepare(manager: ResourceManager): BakedReplacements { + val resources = manager.listResources("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.open()) + .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.lookupOrThrow(Registries.BLOCK) + .get(ResourceKey.create(Registries.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) }) + } + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloadListener(object : + SimplePreparableReloadListener<BakedReplacements>() { + override fun prepare(manager: ResourceManager, profiler: ProfilerFiller): BakedReplacements { + return preparationFuture.join().also { + it.modelBakingFuture.join() + } + } + + override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: ProfilerFiller?) { + allLocationReplacements = prepared + refreshReplacements() + } + }) + } + + fun simpleBlockModel(blockId: ResourceLocation): SingleVariant.Unbaked { + // TODO: does this need to be shared between resolving and baking? I think not, but it would probably be wise to do so in the future. + return SingleVariant.Unbaked( + Variant(blockId) + ) + } + + /** + * Used by [moe.nea.firmament.init.SectionBuilderRiser] + */ + + @JvmStatic + fun patchIndigo(original: BlockStateModel, pos: BlockPos?, state: BlockState): BlockStateModel { + return getReplacementModel(state, pos) ?: original + } + + @JvmStatic + fun collectExtraModels(modelsCollector: ModelDiscovery) { + preparationFuture.join().collectAllReplacements() + .forEach { + modelsCollector.addRoot(simpleBlockModel(it.blockModelIdentifier)) + it.unbakedBlockStateMap?.values?.forEach { + modelsCollector.addRoot(it) + } + } + } + + @JvmStatic + fun createBakedModels(baker: ModelBaker, executor: Executor): CompletableFuture<Void?> { + return preparationFuture.thenComposeAsync(Function { replacements -> + val allBlockStates = CompletableFuture.allOf( + *replacements.collectAllReplacements().filter { it.unbakedBlockStateMap != null }.map { + CompletableFuture.supplyAsync({ + it.blockStateMap = it.unbakedBlockStateMap + ?.map { + it.key to it.value.bake(it.key, baker) + } + ?.toMap() + }, executor) + }.toList().toTypedArray() + ) + val byModel = replacements.collectAllReplacements().groupBy { it.blockModelIdentifier } + val modelBakingTask = ParallelMapTransform.schedule(byModel, { blockId, replacements -> + val unbakedModel = SingleVariant.Unbaked( + Variant(blockId) + ) + val baked = unbakedModel.bake(baker) + replacements.forEach { + it.blockModel = baked + } + }, executor) + modelBakingTask.thenComposeAsync { + allBlockStates + }.thenAcceptAsync { + replacements.modelBakingFuture.complete(Unit) + } + }, executor) + } + + @JvmStatic + fun collectExtraBlockStateMaps( + extra: BakedReplacements, + original: Map<ResourceLocation, List<Resource>>, + stateManagers: Function<ResourceLocation, StateDefinition<Block, BlockState>?> + ) { + extra.collectAllReplacements().forEach { + val blockId = BuiltInRegistries.BLOCK.getResourceKey(it.overridingBlock).getOrNull()?.location() ?: return@forEach + val allModels = mutableListOf<BlockStateModelLoader.LoadedBlockModelDefinition>() + val stateManager = stateManagers.apply(blockId) ?: return@forEach + for (resource in original[BlockStateModelLoader.BLOCKSTATE_LISTER.idToFile(it.block)] ?: return@forEach) { + try { + resource.openAsReader().use { reader -> + val jsonElement = JsonParser.parseReader(reader) + val blockModelDefinition = + BlockModelDefinition.CODEC.parse(JsonOps.INSTANCE, jsonElement) + .getOrThrow { msg: String? -> JsonParseException(msg) } + allModels.add( + BlockStateModelLoader.LoadedBlockModelDefinition( + resource.sourcePackId(), + blockModelDefinition + ) + ) + } + } catch (exception: Exception) { + ErrorUtil.softError( + "Failed to load custom blockstate definition ${it.block} from pack ${resource.sourcePackId()}", + exception + ) + } + } + + try { + it.unbakedBlockStateMap = BlockStateModelLoader.loadBlockStateDefinitionStack( + blockId, + stateManager, + allModels + ).models + } catch (exception: Exception) { + ErrorUtil.softError("Failed to combine custom blockstate definitions for ${it.block}", exception) + } + } + } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTexturesDebugger.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTexturesDebugger.kt new file mode 100644 index 0000000..ba19e0f --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTexturesDebugger.kt @@ -0,0 +1,132 @@ +package moe.nea.firmament.features.texturepack + +import com.mojang.brigadier.arguments.IntegerArgumentType +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager +import net.minecraft.world.level.block.Block +import net.minecraft.commands.arguments.blocks.BlockStateParser +import net.minecraft.commands.arguments.blocks.BlockStateArgument +import net.minecraft.world.phys.AABB +import net.minecraft.world.phys.Vec3 +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.commands.get +import moe.nea.firmament.commands.thenArgument +import moe.nea.firmament.commands.thenExecute +import moe.nea.firmament.commands.thenLiteral +import moe.nea.firmament.events.CommandEvent +import moe.nea.firmament.events.WorldRenderLastEvent +import moe.nea.firmament.features.debug.DeveloperFeatures +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.render.RenderInWorldContext +import moe.nea.firmament.util.tr + +object CustomBlockTexturesDebugger { + var debugMode: DebugMode = DebugMode.Never + var range = 30 + + + @Subscribe + fun onRender(event: WorldRenderLastEvent) { + if (debugMode == DebugMode.Never) return + val replacements = CustomBlockTextures.currentIslandReplacements ?: return + RenderInWorldContext.renderInWorld(event) { + for ((block, repl) in replacements.lookup) { + if (!debugMode.shouldHighlight(block)) continue + for (i in repl) { + if (i.roughCheck != null) + tryRenderBox(i.roughCheck!!.toBox(), 0x50FF8050.toInt()) + i.checks?.forEach { area -> + tryRenderBox(area.toBox(), 0x5050FF50.toInt()) + } + } + } + } + } + + fun RenderInWorldContext.tryRenderBox(box: AABB, colour: Int) { + val player = MC.player?.position ?: Vec3.ZERO + if (box.center.distanceTo(player) < range + maxOf( + box.zsize, box.xsize, box.ysize + ) / 2 && !box.contains(player) + ) { + box(box, colour) + } + } + + + @Subscribe + fun onCommand(event: CommandEvent.SubCommand) { + event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) { + thenLiteral("debugcbt") { + thenLiteral("range") { + thenArgument("range", IntegerArgumentType.integer(0)) { rangeArg -> + thenExecute { + this@CustomBlockTexturesDebugger.range = get(rangeArg) + MC.sendChat( + tr( + "firmament.debugcbt.always", + "Only render areas within ${this@CustomBlockTexturesDebugger.range} blocks" + ) + ) + } + } + } + thenLiteral("all") { + thenExecute { + debugMode = DebugMode.Always + MC.sendChat( + tr( + "firmament.debugcbt.always", + "Showing debug outlines for all custom block textures" + ) + ) + } + } + thenArgument("block", BlockStateArgument.block(event.commandRegistryAccess)) { block -> + thenExecute { + val block = get(block).state.block + debugMode = DebugMode.ForBlock(block) + MC.sendChat( + tr( + "firmament.debugcbt.block", + "Showing debug outlines for all custom ${block.name} textures" + ) + ) + } + } + thenLiteral("never") { + thenExecute { + debugMode = DebugMode.Never + MC.sendChat( + tr( + "firmament.debugcbt.disabled", + "Disabled debug outlines for custom block textures" + ) + ) + } + } + } + } + } + + sealed interface DebugMode { + fun shouldHighlight(block: Block): Boolean + + data object Never : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return false + } + } + + data class ForBlock(val block: Block) : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return block == this.block + } + } + + data object Always : DebugMode { + override fun shouldHighlight(block: Block): Boolean { + return true + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt index 85dfa32..7e3c018 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt @@ -8,16 +8,16 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.UseSerializers -import net.minecraft.client.render.entity.equipment.EquipmentModel -import net.minecraft.component.type.EquippableComponent -import net.minecraft.entity.EquipmentSlot -import net.minecraft.item.ItemStack -import net.minecraft.item.equipment.EquipmentAssetKeys -import net.minecraft.registry.RegistryKey -import net.minecraft.resource.ResourceManager -import net.minecraft.resource.SinglePreparationResourceReloader -import net.minecraft.util.Identifier -import net.minecraft.util.profiler.Profiler +import net.minecraft.client.resources.model.EquipmentClientInfo +import net.minecraft.world.item.equipment.Equippable +import net.minecraft.world.entity.EquipmentSlot +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.equipment.EquipmentAssets +import net.minecraft.resources.ResourceKey +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FinalizeResourceManagerEvent @@ -30,14 +30,14 @@ import moe.nea.firmament.util.skyBlockId object CustomGlobalArmorOverrides { @Serializable data class ArmorOverride( - @SerialName("item_ids") + @SerialName("item_ids") val itemIds: List<String>, - val layers: List<ArmorOverrideLayer>? = null, - val model: Identifier? = null, - val overrides: List<ArmorOverrideOverride> = listOf(), + val layers: List<ArmorOverrideLayer>? = null, + val model: ResourceLocation? = null, + val overrides: List<ArmorOverrideOverride> = listOf(), ) { @Transient - lateinit var modelIdentifier: Identifier + lateinit var modelIdentifier: ResourceLocation fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) overrides.forEach { it.bake(manager) } @@ -51,16 +51,16 @@ object CustomGlobalArmorOverrides { @Serializable data class ArmorOverrideLayer( - val tint: Boolean = false, - val identifier: Identifier, - val suffix: String = "", + val tint: Boolean = false, + val identifier: ResourceLocation, + val suffix: String = "", ) @Serializable data class ArmorOverrideOverride( - val predicate: FirmamentModelPredicate, - val layers: List<ArmorOverrideLayer>? = null, - val model: Identifier? = null, + val predicate: FirmamentModelPredicate, + val layers: List<ArmorOverrideLayer>? = null, + val model: ResourceLocation? = null, ) { init { require(layers != null || model != null) { "Either model or layers must be specified for armor override override" } @@ -68,60 +68,69 @@ object CustomGlobalArmorOverrides { } @Transient - lateinit var modelIdentifier: Identifier + lateinit var modelIdentifier: ResourceLocation fun bake(manager: ResourceManager) { modelIdentifier = bakeModel(model, layers) } } - private fun resolveComponent(slot: EquipmentSlot, model: Identifier): EquippableComponent { - return EquippableComponent( + private fun resolveComponent(slot: EquipmentSlot, model: ResourceLocation): Equippable { + return Equippable( slot, null, - Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)), + Optional.of(ResourceKey.create(EquipmentAssets.ROOT_ID, model)), Optional.empty(), - Optional.empty(), false, false, false + Optional.empty(), + false, + false, + false, + false, + false, + null ) } + // TODO: BipedEntityRenderer.getEquippedStack create copies of itemstacks for rendering. This means this cache is essentially useless + // If i figure out how to circumvent this (maybe track the origin of those copied itemstacks in some sort of variable in the itemstack to track back the original instance) i should reenable this cache. + // Then also re add this to the cache clearing function val overrideCache = - WeakCache.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot -> - val id = stack.skyBlockId ?: return@memoize Optional.empty() - val override = overrides[id.neuItem] ?: return@memoize Optional.empty() + WeakCache.dontMemoize<ItemStack, EquipmentSlot, Optional<Equippable>>("ArmorOverrides") { stack, slot -> + val id = stack.skyBlockId ?: return@dontMemoize Optional.empty() + val override = overrides[id.neuItem] ?: return@dontMemoize Optional.empty() for (suboverride in override.overrides) { if (suboverride.predicate.test(stack)) { - return@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional() } } - return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional() + return@dontMemoize resolveComponent(slot, override.modelIdentifier).intoOptional() } var overrides: Map<String, ArmorOverride> = mapOf() - private var bakedOverrides: MutableMap<Identifier, EquipmentModel> = mutableMapOf() + private var bakedOverrides: MutableMap<ResourceLocation, EquipmentClientInfo> = mutableMapOf() private val sentinelFirmRunning = AtomicInteger() - private fun bakeModel(model: Identifier?, layers: List<ArmorOverrideLayer>?): Identifier { + private fun bakeModel(model: ResourceLocation?, layers: List<ArmorOverrideLayer>?): ResourceLocation { require(model == null || layers == null) if (model != null) { return model } else if (layers != null) { val idNumber = sentinelFirmRunning.incrementAndGet() - val identifier = Identifier.of("firmament:sentinel/armor/$idNumber") + val identifier = ResourceLocation.parse("firmament:sentinel/armor/$idNumber") val equipmentLayers = layers.map { - EquipmentModel.Layer( + EquipmentClientInfo.Layer( it.identifier, if (it.tint) { - Optional.of(EquipmentModel.Dyeable(Optional.empty())) + Optional.of(EquipmentClientInfo.Dyeable(Optional.of(0xFFA06540.toInt()))) } else { Optional.empty() }, false ) } - bakedOverrides[identifier] = EquipmentModel( + bakedOverrides[identifier] = EquipmentClientInfo( mapOf( - EquipmentModel.LayerType.HUMANOID to equipmentLayers, - EquipmentModel.LayerType.HUMANOID_LEGGINGS to equipmentLayers, + EquipmentClientInfo.LayerType.HUMANOID to equipmentLayers, + EquipmentClientInfo.LayerType.HUMANOID_LEGGINGS to equipmentLayers, ) ) return identifier @@ -133,38 +142,40 @@ object CustomGlobalArmorOverrides { @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") { + event.resourceManager.registerReloadListener(object : + SimplePreparableReloadListener<Map<String, ArmorOverride>>() { + override fun prepare(manager: ResourceManager, profiler: ProfilerFiller): Map<String, ArmorOverride> { + val overrideFiles = manager.listResources("overrides/armor_models") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } val overrides = overrideFiles.mapNotNull { - Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex -> + Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.open()).getOrElse { ex -> logger.error("Failed to load armor texture override at ${it.key}", ex) null } } + bakedOverrides.clear() val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } } .toMap() associatedMap.forEach { it.value.bake(manager) } return associatedMap } - override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) { - bakedOverrides.clear() + override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: ProfilerFiller) { overrides = prepared } }) } @JvmStatic - fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<EquippableComponent> { + fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<Equippable> { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return Optional.empty() return overrideCache.invoke(itemStack, slot) } @JvmStatic - fun overrideArmorLayer(id: Identifier): EquipmentModel? { + fun overrideArmorLayer(id: ResourceLocation): EquipmentClientInfo? { + if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null return bakedOverrides[id] } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt index 02c0714..60a6c06 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -8,40 +8,33 @@ import org.slf4j.LoggerFactory import kotlinx.serialization.Serializable import kotlinx.serialization.UseSerializers import kotlin.jvm.optionals.getOrNull -import net.minecraft.client.util.ModelIdentifier -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 net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent 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.ErrorUtil import moe.nea.firmament.util.IdentifierSerializer import moe.nea.firmament.util.MC 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 - +object CustomGlobalTextures : SimplePreparableReloadListener<CustomGlobalTextures.CustomGuiTextureOverride>() { class CustomGuiTextureOverride( val classes: List<ItemOverrideCollection> ) @Serializable data class GlobalItemOverride( - val screen: @Serializable(SingletonSerializableList::class) List<Identifier>, - val model: Identifier, - val predicate: FirmamentModelPredicate, + val screen: @Serializable(SingletonSerializableList::class) List<ResourceLocation>, + val model: ResourceLocation, + val predicate: FirmamentModelPredicate, ) @Serializable @@ -56,7 +49,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText @Subscribe fun onStart(event: FinalizeResourceManagerEvent) { - MC.resourceManager.registerReloader(this) + MC.resourceManager.registerReloadListener(this) } @Subscribe @@ -65,27 +58,29 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText .supplyAsync( { prepare(event.resourceManager) - }, event.preparationExecutor) + }, event.preparationExecutor + ) } @Volatile var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture( - CustomGuiTextureOverride(listOf())) + CustomGuiTextureOverride(listOf()) + ) - override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride { + override fun prepare(manager: ResourceManager?, profiler: ProfilerFiller?): CustomGuiTextureOverride { return preparationFuture.join() } - override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) { + override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: ProfilerFiller?) { 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") } + manager.listResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } .mapNotNull { - Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex -> + Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.open()).getOrElse { ex -> ErrorUtil.softError("Failed to load global item override at ${it.key}", ex) null } @@ -97,15 +92,18 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText .mapNotNull { val key = it.key val guiClassResource = - manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json")) + manager.getResource(ResourceLocation.fromNamespaceAndPath(key.namespace, "filters/screen/${key.path}.json")) .getOrNull() ?: return@mapNotNull runNull { - ErrorUtil.softError("Failed to locate screen filter at $key") + ErrorUtil.softError("Failed to locate screen filter at $key used by ${it.value.map { it.first }}") } val screenFilter = - Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream) + Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.open()) .getOrElse { ex -> - ErrorUtil.softError("Failed to load screen filter at $key", ex) + ErrorUtil.softError( + "Failed to load screen filter at $key used by ${it.value.map { it.first }}", + ex + ) return@mapNotNull null } ItemOverrideCollection(screenFilter, it.value.map { it.second }) @@ -120,7 +118,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText @Subscribe fun onOpenGui(event: ScreenChangeEvent) { - val newTitle = event.new?.title ?: Text.empty() + val newTitle = event.new?.title ?: Component.empty() matchingOverrides = guiClassOverrides.classes .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt index fca8944..2d615c2 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -7,9 +7,9 @@ import com.mojang.serialization.DataResult import com.mojang.serialization.Decoder import com.mojang.serialization.DynamicOps import com.mojang.serialization.Encoder -import net.minecraft.client.render.item.model.ItemModelTypes -import net.minecraft.item.ItemStack -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.item.ItemModels +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FinalizeResourceManagerEvent @@ -17,11 +17,14 @@ import moe.nea.firmament.features.texturepack.predicates.AndPredicate import moe.nea.firmament.features.texturepack.predicates.CastPredicate import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate +import moe.nea.firmament.features.texturepack.predicates.GenericComponentPredicate import moe.nea.firmament.features.texturepack.predicates.ItemPredicate import moe.nea.firmament.features.texturepack.predicates.LorePredicate import moe.nea.firmament.features.texturepack.predicates.NotPredicate import moe.nea.firmament.features.texturepack.predicates.OrPredicate import moe.nea.firmament.features.texturepack.predicates.PetPredicate +import moe.nea.firmament.features.texturepack.predicates.PullingPredicate +import moe.nea.firmament.features.texturepack.predicates.SkullPredicate import moe.nea.firmament.util.json.KJsonOps object CustomModelOverrideParser { @@ -46,11 +49,11 @@ object CustomModelOverrideParser { } ) - val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>() + val predicateParsers = mutableMapOf<ResourceLocation, FirmamentModelPredicateParser>() fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) { - predicateParsers[Identifier.of("firmament", name)] = parser + predicateParsers[ResourceLocation.fromNamespaceAndPath("firmament", name)] = parser } init { @@ -62,6 +65,8 @@ object CustomModelOverrideParser { registerPredicateParser("item", ItemPredicate.Parser) registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser) registerPredicateParser("pet", PetPredicate.Parser) + registerPredicateParser("component", GenericComponentPredicate.Parser) + registerPredicateParser("skull", SkullPredicate.Parser) } private val neverPredicate = listOf( @@ -79,8 +84,14 @@ object CustomModelOverrideParser { if (predicateName == "cast") { // 1.21.4 parsedPredicates.add(CastPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) } + if (predicateName == "pull") { + parsedPredicates.add(PullingPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate) + } + if (predicateName == "pulling") { + parsedPredicates.add(PullingPredicate.AnyPulling) + } if (!predicateName.startsWith("firmament:")) continue - val identifier = Identifier.of(predicateName) + val identifier = ResourceLocation.parse(predicateName) val parser = predicateParsers[identifier] ?: return neverPredicate val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate parsedPredicates.add(parsedPredicate) @@ -99,10 +110,14 @@ object CustomModelOverrideParser { @Subscribe fun finalizeResources(event: FinalizeResourceManagerEvent) { - ItemModelTypes.ID_MAPPER.put( + ItemModels.ID_MAPPER.put( Firmament.identifier("predicates/legacy"), PredicateModel.Unbaked.CODEC ) + ItemModels.ID_MAPPER.put( + Firmament.identifier("head_model"), + HeadModelChooser.Unbaked.CODEC + ) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt new file mode 100644 index 0000000..e026365 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt @@ -0,0 +1,257 @@ +package moe.nea.firmament.features.texturepack + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.client.gui.Font +import net.minecraft.client.renderer.RenderPipelines +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.inventory.AbstractSignEditScreen +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.client.gui.screens.inventory.HangingSignEditScreen +import net.minecraft.client.gui.screens.inventory.SignEditScreen +import net.minecraft.client.renderer.RenderType +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.Slot +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.CENTER +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.LEFT +import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.RIGHT +import moe.nea.firmament.mixins.accessor.AccessorHandledScreen +import moe.nea.firmament.mixins.accessor.AccessorScreenHandler +import moe.nea.firmament.util.ErrorUtil.intoCatch +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.accessors.castAccessor + +object CustomScreenLayouts : SimplePreparableReloadListener<List<CustomScreenLayouts.CustomScreenLayout>>() { + + @Serializable + data class CustomScreenLayout( + val predicates: Preds, + val background: BackgroundReplacer? = null, + val slots: List<SlotReplacer> = listOf(), + val playerTitle: TitleReplacer? = null, + val containerTitle: TitleReplacer? = null, + val repairCostTitle: TitleReplacer? = null, + val nameField: ComponentMover? = null, + val signLines: List<ComponentMover>? = null, + ) { + init { + if (signLines != null) + require(signLines.size == 4) + } + } + + @Serializable + data class ComponentMover( + val x: Int, + val y: Int, + val width: Int? = null, + val height: Int? = null, + ) + + @Serializable + data class Preds( + val label: StringMatcher, + @Serializable(with = IdentifierSerializer::class) + val screenType: ResourceLocation? = null, + ) { + fun matches(screen: Screen): Boolean { + // TODO: does this deserve the restriction to handled screen + val type = when (screen) { + is AbstractContainerScreen<*> -> (screen.menu as AccessorScreenHandler).type_firmament?.let { + BuiltInRegistries.MENU.getKey(it) + } + + is HangingSignEditScreen -> ResourceLocation.fromNamespaceAndPath("firmskyblock", "hanging_sign") + is SignEditScreen -> ResourceLocation.fromNamespaceAndPath("firmskyblock", "sign") + else -> null + } + val typeMatches = screenType == null || type == screenType; + return label.matches(screen.title) && typeMatches + } + } + + @Serializable + data class BackgroundReplacer( + @Serializable(with = IdentifierSerializer::class) + val texture: ResourceLocation, + // TODO: allow selectively still rendering some components (recipe button, trade backgrounds, furnace flame progress, arrows) + val x: Int, + val y: Int, + val width: Int, + val height: Int, + ) { + fun renderDirect(context: GuiGraphics) { + context.blit( + RenderPipelines.GUI_TEXTURED, + this.texture, + this.x, this.y, + 0F, 0F, + this.width, this.height, this.width, this.height, + ) + } + + fun renderGeneric(context: GuiGraphics, screen: AbstractContainerScreen<*>) { + screen.castAccessor() + val originalX: Int = (screen.width - screen.backgroundWidth_Firmament) / 2 + val originalY: Int = (screen.height - screen.backgroundHeight_Firmament) / 2 + val modifiedX = originalX + this.x + val modifiedY = originalY + this.y + val textureWidth = this.width + val textureHeight = this.height + context.blit( + RenderPipelines.GUI_TEXTURED, + this.texture, + modifiedX, + modifiedY, + 0.0f, + 0.0f, + textureWidth, + textureHeight, + textureWidth, + textureHeight + ) + + } + } + + @Serializable + data class SlotReplacer( + // TODO: override getRecipeBookButtonPos as well + // TODO: is this index or id (i always forget which one is duplicated per inventory) + val index: Int, + val x: Int, + val y: Int, + ) { + fun move(slots: List<Slot>) { + val slot = slots.getOrNull(index) ?: return + slot.x = x + slot.y = y + } + } + + @Serializable + enum class Alignment { + @SerialName("left") + LEFT, + + @SerialName("center") + CENTER, + + @SerialName("right") + RIGHT + } + + @Serializable + data class TitleReplacer( + val x: Int? = null, + val y: Int? = null, + val align: Alignment = Alignment.LEFT, + val replace: String? = null + ) { + @Transient + val replacedText: Component? = replace?.let(Component::literal) + + fun replaceText(text: Component): Component { + if (replacedText != null) return replacedText + return text + } + + fun replaceY(y: Int): Int { + return this.y ?: y + } + + fun replaceX(font: Font, text: Component, x: Int): Int { + val baseX = this.x ?: x + return baseX + when (this.align) { + LEFT -> 0 + CENTER -> -font.width(text) / 2 + RIGHT -> -font.width(text) + } + } + + /** + * Not technically part of the package, but it does allow for us to later on seamlessly integrate a color option into this class as well + */ + fun replaceColor(text: Component, color: Int): Int { + return CustomTextColors.mapTextColor(text, color) + } + } + + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloadListener(CustomScreenLayouts) + } + + override fun prepare( + manager: ResourceManager, + profiler: ProfilerFiller + ): List<CustomScreenLayout> { + val allScreenLayouts = manager.listResources( + "overrides/screen_layout", + { it.path.endsWith(".json") && it.namespace == "firmskyblock" }) + val allParsedLayouts = allScreenLayouts.mapNotNull { (path, stream) -> + Firmament.tryDecodeJsonFromStream<CustomScreenLayout>(stream.open()) + .intoCatch("Could not read custom screen layout from $path").orNull() + } + return allParsedLayouts + } + + var customScreenLayouts = listOf<CustomScreenLayout>() + + override fun apply( + prepared: List<CustomScreenLayout>, + manager: ResourceManager?, + profiler: ProfilerFiller? + ) { + this.customScreenLayouts = prepared + } + + @get:JvmStatic + var activeScreenOverride = null as CustomScreenLayout? + + val DO_NOTHING_TEXT_REPLACER = TitleReplacer() + + @JvmStatic + fun <T> getMover(selector: (CustomScreenLayout) -> (T?)) = + activeScreenOverride?.let(selector) + + @JvmStatic + fun getSignTextMover(index: Int) = + getMover { it.signLines?.get(index) } + + @JvmStatic + fun getTextMover(selector: (CustomScreenLayout) -> (TitleReplacer?)) = + getMover(selector) ?: DO_NOTHING_TEXT_REPLACER + + @Subscribe + fun onScreenOpen(event: ScreenChangeEvent) { + if (!CustomSkyBlockTextures.TConfig.allowLayoutChanges) { + activeScreenOverride = null + return + } + activeScreenOverride = event.new?.let { screen -> + customScreenLayouts.find { it.predicates.matches(screen) } + } + + val screen = event.new as? AbstractContainerScreen<*> ?: return + val handler = screen.menu + activeScreenOverride?.let { override -> + override.slots.forEach { slotReplacer -> + slotReplacer.move(handler.slots) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt index d9ca5b4..a324402 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -3,29 +3,29 @@ package moe.nea.firmament.features.texturepack import com.mojang.authlib.minecraft.MinecraftProfileTexture import com.mojang.authlib.properties.Property import java.util.Optional -import org.jetbrains.annotations.Nullable import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable import kotlin.jvm.optionals.getOrNull -import net.minecraft.block.SkullBlock -import net.minecraft.client.MinecraftClient -import net.minecraft.client.render.RenderLayer -import net.minecraft.component.type.ProfileComponent -import net.minecraft.util.Identifier +import net.minecraft.world.level.block.SkullBlock +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.RenderType +import net.minecraft.world.item.component.ResolvableProfile +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.events.TickEvent -import moe.nea.firmament.features.FirmamentFeature import moe.nea.firmament.features.debug.PowerUserTools -import moe.nea.firmament.gui.config.ManagedConfig import moe.nea.firmament.util.collections.WeakCache +import moe.nea.firmament.util.data.Config +import moe.nea.firmament.util.data.ManagedConfig import moe.nea.firmament.util.mc.decodeProfileTextureProperty import moe.nea.firmament.util.skyBlockId -object CustomSkyBlockTextures : FirmamentFeature { - override val identifier: String +object CustomSkyBlockTextures { + val identifier: String get() = "custom-skyblock-textures" + @Config object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing? val enabled by toggle("enabled") { true } val skullsEnabled by toggle("skulls-enabled") { true } @@ -34,17 +34,17 @@ object CustomSkyBlockTextures : FirmamentFeature { val enableModelOverrides by toggle("model-overrides") { true } val enableArmorOverrides by toggle("armor-overrides") { true } val enableBlockOverrides by toggle("block-overrides") { true } + val enableLegacyMinecraftCompat by toggle("legacy-minecraft-path-support") { true } val enableLegacyCIT by toggle("legacy-cit") { true } val allowRecoloringUiText by toggle("recolor-text") { true } + val allowLayoutChanges by toggle("screen-layouts") { true } } - override val config: ManagedConfig - get() = TConfig - val allItemCaches by lazy { listOf( skullTextureCache.cache, - CustomGlobalArmorOverrides.overrideCache.cache + CustomItemModelEvent.cache.cache, + // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache ) } @@ -75,13 +75,13 @@ object CustomSkyBlockTextures : FirmamentFeature { fun onCustomModelId(it: CustomItemModelEvent) { if (!TConfig.enabled) return val id = it.itemStack.skyBlockId ?: return - it.overrideIfExists(Identifier.of("firmskyblock", id.identifier.path)) + it.overrideIfEmpty(ResourceLocation.fromNamespaceAndPath("firmskyblock", id.identifier.path)) } private val skullTextureCache = - WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component -> + WeakCache.memoize<ResolvableProfile, Optional<ResourceLocation>>("SkullTextureCache") { component -> val id = getSkullTexture(component) ?: return@memoize Optional.empty() - if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) { + if (!Minecraft.getInstance().resourceManager.getResource(id).isPresent) { return@memoize Optional.empty() } return@memoize Optional.of(id) @@ -97,21 +97,21 @@ object CustomSkyBlockTextures : FirmamentFeature { 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 getSkullTexture(profile: ResolvableProfile): ResourceLocation? { + val id = getSkullId(profile.partialProfile().properties["textures"].firstOrNull() ?: return null) ?: return null + return ResourceLocation.fromNamespaceAndPath("firmskyblock", "textures/placedskull/$id.png") } fun modifySkullTexture( - type: SkullBlock.SkullType?, - component: ProfileComponent?, - cir: CallbackInfoReturnable<RenderLayer> + type: SkullBlock.Type?, + component: ResolvableProfile?, + cir: CallbackInfoReturnable<RenderType> ) { - if (type != SkullBlock.Type.PLAYER) return + if (type != SkullBlock.Types.PLAYER) return if (!TConfig.skullsEnabled) return if (component == null) return val n = skullTextureCache.invoke(component).getOrNull() ?: return - cir.returnValue = RenderLayer.getEntityTranslucent(n) + cir.returnValue = RenderType.entityTranslucent(n) } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt index 4ca1796..5f3b08d 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt @@ -2,52 +2,66 @@ package moe.nea.firmament.features.texturepack import java.util.Optional import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import kotlin.jvm.optionals.getOrNull -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 net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.profiling.ProfilerFiller import moe.nea.firmament.Firmament import moe.nea.firmament.annotations.Subscribe import moe.nea.firmament.events.FinalizeResourceManagerEvent import moe.nea.firmament.util.collections.WeakCache -object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.TextOverrides?>() { +object CustomTextColors : SimplePreparableReloadListener<CustomTextColors.TextOverrides?>() { @Serializable data class TextOverrides( val defaultColor: Int, val overrides: List<TextOverride> = listOf() - ) + ) { + /** + * Stub custom text color to allow always returning a text override + */ + @Transient + val baseOverride = TextOverride( + StringMatcher.Equals("", false), + defaultColor, + 0, + 0 + ) + } @Serializable data class TextOverride( val predicate: StringMatcher, val override: Int, + val x: Int = 0, + val y: Int = 0, ) @Subscribe fun registerTextColorReloader(event: FinalizeResourceManagerEvent) { - event.resourceManager.registerReloader(this) + event.resourceManager.registerReloadListener(this) } - val cache = WeakCache.memoize<Text, Optional<Int>>("CustomTextColor") { text -> + val cache = WeakCache.memoize<Component, Optional<TextOverride>>("CustomTextColor") { text -> val override = textOverrides ?: return@memoize Optional.empty() - Optional.of(override.overrides.find { it.predicate.matches(text) }?.override ?: override.defaultColor) + Optional.ofNullable(override.overrides.find { it.predicate.matches(text) }) } - fun mapTextColor(text: Text, oldColor: Int): Int { - if (textOverrides == null) return oldColor - return cache(text).getOrNull() ?: oldColor + fun mapTextColor(text: Component, oldColor: Int): Int { + val override = cache(text).orElse(null) + return override?.override ?: textOverrides?.defaultColor ?: oldColor } override fun prepare( - manager: ResourceManager, - profiler: Profiler + manager: ResourceManager, + profiler: ProfilerFiller ): TextOverrides? { - val resource = manager.getResource(Identifier.of("firmskyblock", "overrides/text_colors.json")).getOrNull() + val resource = manager.getResource(ResourceLocation.fromNamespaceAndPath("firmskyblock", "overrides/text_colors.json")).getOrNull() ?: return null - return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.inputStream) + return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.open()) .getOrElse { Firmament.logger.error("Could not parse text_colors.json", it) null @@ -57,9 +71,9 @@ object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.Tex var textOverrides: TextOverrides? = null override fun apply( - prepared: TextOverrides?, - manager: ResourceManager, - profiler: Profiler + prepared: TextOverrides?, + manager: ResourceManager, + profiler: ProfilerFiller ) { textOverrides = prepared } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt new file mode 100644 index 0000000..71617fd --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextReplacements.kt @@ -0,0 +1,56 @@ +package moe.nea.firmament.features.texturepack + +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.network.chat.Component +import net.minecraft.util.profiling.ProfilerFiller +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.util.ErrorUtil.intoCatch + +object CustomTextReplacements : SimplePreparableReloadListener<List<TreeishTextReplacer>>() { + + override fun prepare( + manager: ResourceManager, + profiler: ProfilerFiller + ): List<TreeishTextReplacer> { + return manager.listResources("overrides/texts") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } + .mapNotNull { + Firmament.tryDecodeJsonFromStream<TreeishTextReplacer>(it.value.open()) + .intoCatch("Failed to load text override from ${it.key}").orNull() + } + } + + var textReplacers: List<TreeishTextReplacer> = listOf() + + override fun apply( + prepared: List<TreeishTextReplacer>, + manager: ResourceManager, + profiler: ProfilerFiller + ) { + this.textReplacers = prepared + } + + @JvmStatic + fun replaceTexts(texts: List<Component>): List<Component> { + return texts.map { replaceText(it) } + } + + @JvmStatic + fun replaceText(text: Component): Component { + // TODO: add a config option for this + val rawText = text.string + var text = text + for (replacer in textReplacers) { + if (!replacer.match.matches(rawText)) continue + text = replacer.replaceText(text) + } + return text + } + + @Subscribe + fun onReloadStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloadListener(this) + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt index d11fec0..d10b6e9 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt @@ -1,8 +1,11 @@ - package moe.nea.firmament.features.texturepack -import net.minecraft.item.ItemStack +import kotlinx.serialization.Serializable +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +@Serializable(with = FirmamentRootPredicateSerializer::class) interface FirmamentModelPredicate { - fun test(stack: ItemStack): Boolean + fun test(stack: ItemStack, holder: LivingEntity?): Boolean = test(stack) + fun test(stack: ItemStack): Boolean = test(stack, null) } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt index 0b8ae8e..39e1fc3 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentRootPredicateSerializer.kt @@ -6,6 +6,7 @@ import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import moe.nea.firmament.features.texturepack.predicates.AndPredicate +import moe.nea.firmament.util.json.intoGson object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> { val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt new file mode 100644 index 0000000..03496aa --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt @@ -0,0 +1,92 @@ +package moe.nea.firmament.features.texturepack + +import com.google.gson.JsonObject +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.minecraft.client.renderer.item.ItemModelResolver +import net.minecraft.client.renderer.item.ItemStackRenderState +import net.minecraft.client.renderer.item.BlockModelWrapper +import net.minecraft.client.renderer.item.ItemModel +import net.minecraft.client.renderer.item.ItemModels +import net.minecraft.client.resources.model.ResolvableModel +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.ItemStack +import net.minecraft.world.entity.ItemOwner +import net.minecraft.resources.ResourceLocation + +object HeadModelChooser { + val IS_CHOOSING_HEAD_MODEL = ThreadLocal.withInitial { false } + + interface HasExplicitHeadModelMarker { + fun markExplicitHead_Firmament() + fun isExplicitHeadModel_Firmament(): Boolean + companion object{ + @JvmStatic + fun cast(state: ItemStackRenderState) = state as HasExplicitHeadModelMarker + } + } + + data class Baked(val head: ItemModel, val regular: ItemModel) : ItemModel { + + override fun update( + state: ItemStackRenderState, + stack: ItemStack?, + resolver: ItemModelResolver?, + displayContext: ItemDisplayContext, + world: ClientLevel?, + heldItemContext: ItemOwner?, + seed: Int + ) { + val instance = + if (IS_CHOOSING_HEAD_MODEL.get()) { + HasExplicitHeadModelMarker.cast(state).markExplicitHead_Firmament() + head + } else { + regular + } + instance.update(state, stack, resolver, displayContext, world, heldItemContext, seed) + } + } + + data class Unbaked( + val head: ItemModel.Unbaked, + val regular: ItemModel.Unbaked, + ) : ItemModel.Unbaked { + override fun type(): MapCodec<out ItemModel.Unbaked> { + return CODEC + } + + override fun bake(context: ItemModel.BakingContext): ItemModel { + return Baked( + head.bake(context), + regular.bake(context) + ) + } + + override fun resolveDependencies(resolver: ResolvableModel.Resolver) { + head.resolveDependencies(resolver) + regular.resolveDependencies(resolver) + } + + companion object { + @JvmStatic + fun fromLegacyJson(jsonObject: JsonObject, unbakedModel: ItemModel.Unbaked): ItemModel.Unbaked { + val model = jsonObject["firmament:head_model"] ?: return unbakedModel + val modelUrl = model.asJsonPrimitive.asString + val headModel = BlockModelWrapper.Unbaked(ResourceLocation.parse(modelUrl), listOf()) + return Unbaked(headModel, unbakedModel) + } + + val CODEC = RecordCodecBuilder.mapCodec { + it.group( + ItemModels.CODEC.fieldOf("head") + .forGetter(Unbaked::head), + ItemModels.CODEC.fieldOf("regular") + .forGetter(Unbaked::regular), + ).apply(it, ::Unbaked) + } + } + } +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt index b52e96b..e53c9c7 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt @@ -4,51 +4,51 @@ import com.google.gson.JsonObject import com.mojang.serialization.Codec import com.mojang.serialization.MapCodec import com.mojang.serialization.codecs.RecordCodecBuilder -import net.minecraft.client.item.ItemModelManager -import net.minecraft.client.render.item.ItemRenderState -import net.minecraft.client.render.item.model.BasicItemModel -import net.minecraft.client.render.item.model.ItemModel -import net.minecraft.client.render.item.model.ItemModelTypes -import net.minecraft.client.render.item.tint.TintSource -import net.minecraft.client.render.model.ResolvableModel -import net.minecraft.client.world.ClientWorld -import net.minecraft.entity.LivingEntity -import net.minecraft.item.ItemStack -import net.minecraft.item.ModelTransformationMode -import net.minecraft.util.Identifier +import net.minecraft.client.renderer.item.ItemModelResolver +import net.minecraft.client.renderer.item.ItemStackRenderState +import net.minecraft.client.renderer.item.BlockModelWrapper +import net.minecraft.client.renderer.item.ItemModel +import net.minecraft.client.renderer.item.ItemModels +import net.minecraft.client.resources.model.ResolvableModel +import net.minecraft.client.multiplayer.ClientLevel +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemDisplayContext +import net.minecraft.world.item.ItemStack +import net.minecraft.world.entity.ItemOwner +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.features.texturepack.predicates.AndPredicate class PredicateModel { data class Baked( - val fallback: ItemModel, - val overrides: List<Override> + val fallback: ItemModel, + val overrides: List<Override> ) : ItemModel { data class Override( - val model: ItemModel, - val predicate: FirmamentModelPredicate, + val model: ItemModel, + val predicate: FirmamentModelPredicate, ) override fun update( - state: ItemRenderState, - stack: ItemStack, - resolver: ItemModelManager, - transformationMode: ModelTransformationMode, - world: ClientWorld?, - user: LivingEntity?, - seed: Int + state: ItemStackRenderState?, + stack: ItemStack, + resolver: ItemModelResolver?, + displayContext: ItemDisplayContext?, + world: ClientLevel?, + heldItemContext: ItemOwner?, + seed: Int ) { val model = overrides - .find { it.predicate.test(stack) } + .findLast { it.predicate.test(stack, heldItemContext?.asLivingEntity()) } ?.model ?: fallback - model.update(state, stack, resolver, transformationMode, world, user, seed) + model.update(state, stack, resolver, displayContext, world, heldItemContext, seed) } } data class Unbaked( - val fallback: ItemModel.Unbaked, - val overrides: List<Override>, + val fallback: ItemModel.Unbaked, + val overrides: List<Override>, ) : ItemModel.Unbaked { companion object { @JvmStatic @@ -57,10 +57,10 @@ class PredicateModel { val newOverrides = ArrayList<Override>() for (legacyOverride in legacyOverrides) { legacyOverride as JsonObject - val overrideModel = Identifier.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue + val overrideModel = ResourceLocation.tryParse(legacyOverride.get("model")?.asString ?: continue) ?: continue val predicate = CustomModelOverrideParser.parsePredicates(legacyOverride.getAsJsonObject("predicate")) newOverrides.add(Override( - BasicItemModel.Unbaked(overrideModel, listOf()), + BlockModelWrapper.Unbaked(overrideModel, listOf()), AndPredicate(predicate.toTypedArray()) )) } @@ -69,34 +69,34 @@ class PredicateModel { val OVERRIDE_CODEC: Codec<Override> = RecordCodecBuilder.create { it.group( - ItemModelTypes.CODEC.fieldOf("model").forGetter(Override::model), + ItemModels.CODEC.fieldOf("model").forGetter(Override::model), CustomModelOverrideParser.LEGACY_CODEC.fieldOf("predicate").forGetter(Override::predicate), ).apply(it, Unbaked::Override) } val CODEC: MapCodec<Unbaked> = RecordCodecBuilder.mapCodec { it.group( - ItemModelTypes.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback), + ItemModels.CODEC.fieldOf("fallback").forGetter(Unbaked::fallback), OVERRIDE_CODEC.listOf().fieldOf("overrides").forGetter(Unbaked::overrides), ).apply(it, ::Unbaked) } } data class Override( - val model: ItemModel.Unbaked, - val predicate: FirmamentModelPredicate, + val model: ItemModel.Unbaked, + val predicate: FirmamentModelPredicate, ) - override fun resolve(resolver: ResolvableModel.Resolver) { - fallback.resolve(resolver) - overrides.forEach { it.model.resolve(resolver) } + override fun resolveDependencies(resolver: ResolvableModel.Resolver) { + fallback.resolveDependencies(resolver) + overrides.forEach { it.model.resolveDependencies(resolver) } } - override fun getCodec(): MapCodec<out Unbaked> { + override fun type(): MapCodec<out Unbaked> { return CODEC } - override fun bake(context: ItemModel.BakeContext): ItemModel { + override fun bake(context: ItemModel.BakingContext): ItemModel { return Baked( fallback.bake(context), overrides.map { Baked.Override(it.model.bake(context), it.predicate) } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt index 2b13284..52800bd 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -1,4 +1,3 @@ - package moe.nea.firmament.features.texturepack import com.google.gson.JsonArray @@ -13,147 +12,137 @@ 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 net.minecraft.nbt.StringTag +import net.minecraft.network.chat.Component +import moe.nea.firmament.util.json.intoGson +import moe.nea.firmament.util.json.intoKotlinJson 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, 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 - } - } + fun matches(string: String): Boolean + fun matches(text: Component): Boolean { + return matches(text.string) + } + + val asRegex: java.util.regex.Pattern + + fun matchWithGroups(string: String): MatchNamedGroupCollection? + + fun matches(nbt: StringTag): Boolean { + val string = nbt.value + 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) { + // TODO: return matches(TextCodecs.CODEC.parse(MC.defaultRegistryNbtOps, string) ?: return false) + } + return matches(string) + } + + class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher { + override val asRegex by lazy(LazyThreadSafetyMode.PUBLICATION) { input.toPattern(java.util.regex.Pattern.LITERAL) } + 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 matchWithGroups(string: String): MatchNamedGroupCollection? { + if (matches(string)) + return object : MatchNamedGroupCollection { + override fun get(name: String): MatchGroup? { + return null + } + + override fun get(index: Int): MatchGroup? { + return null + } + + override val size: Int + get() = 0 + + override fun isEmpty(): Boolean { + return true + } + + override fun contains(element: MatchGroup?): Boolean { + return false + } + + override fun iterator(): Iterator<MatchGroup?> { + return emptyList<MatchGroup>().iterator() + } + + override fun containsAll(elements: Collection<MatchGroup?>): Boolean { + return elements.isEmpty() + } + } + return null + } + + override fun toString(): String { + return "Equals($expected, stripColorCodes = $stripColorCodes)" + } + } + + class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher { + private val pattern = patternWithColorCodes.toRegex() + override val asRegex = pattern.toPattern() + override fun matches(string: String): Boolean { + return pattern.matches(if (stripColorCodes) string.removeColorCodes() else string) + } + + override fun matchWithGroups(string: String): MatchNamedGroupCollection? { + return pattern.matchEntire(if (stripColorCodes) string.removeColorCodes() else string)?.groups as MatchNamedGroupCollection? + } + + 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, 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") + } + } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt new file mode 100644 index 0000000..ed486f5 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/TreeishTextReplacer.kt @@ -0,0 +1,79 @@ +package moe.nea.firmament.features.texturepack + +import java.util.regex.Matcher +import util.json.CodecSerializer +import kotlinx.serialization.Serializable +import net.minecraft.network.chat.Style +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.ComponentSerialization +import moe.nea.firmament.util.directLiteralStringContent +import moe.nea.firmament.util.transformEachRecursively + +@Serializable +data class TreeishTextReplacer( + val match: StringMatcher, + val replacements: List<SubPartReplacement> +) { + @Serializable + data class SubPartReplacement( + val match: StringMatcher, + val style: @Serializable(StyleSerializer::class) Style? = null, + val replace: @Serializable(TextSerializer::class) Component, + ) + + object TextSerializer : CodecSerializer<Component>(ComponentSerialization.CODEC) + object StyleSerializer : CodecSerializer<Style>(Style.Serializer.CODEC) + companion object { + val pattern = "[$]\\{(?<name>[^}]+)}".toPattern() + fun injectMatchResults(text: Component, matches: Matcher): Component { + return text.transformEachRecursively { it -> + val content = it.directLiteralStringContent ?: return@transformEachRecursively it + val matcher = pattern.matcher(content) + val builder = StringBuilder() + while (matcher.find()) { + matcher.appendReplacement(builder, matches.group(matcher.group("name")).toString()) + } + matcher.appendTail(builder) + Component.literal(builder.toString()).setStyle(it.style) + } + } + } + + fun match(text: Component): Boolean { + return match.matches(text) + } + + fun replaceText(text: Component): Component { + return text.transformEachRecursively { part -> + var part: Component = part + for (replacement in replacements) { + val rawPartText = part.string + replacement.style?.let { expectedStyle -> + val parentStyle = part.style + val parented = expectedStyle.applyTo(parentStyle) + if (parented.isStrikethrough != parentStyle.isStrikethrough + || parented.isObfuscated != parentStyle.isObfuscated + || parented.isBold != parentStyle.isBold + || parented.isUnderlined != parentStyle.isUnderlined + || parented.isItalic != parentStyle.isItalic + || parented.color?.value != parentStyle.color?.value) + continue + } + val matcher = replacement.match.asRegex.matcher(rawPartText) + if (!matcher.find()) continue + val p = Component.literal("") + p.setStyle(part.style) + var lastAppendPosition = 0 + do { + p.append(rawPartText.substring(lastAppendPosition, matcher.start())) + lastAppendPosition = matcher.end() + p.append(injectMatchResults(replacement.replace, matcher)) + } while (matcher.find()) + p.append(rawPartText.substring(lastAppendPosition)) + part = p + } + part + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt index 7e0ddb1..f32b673 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AlwaysPredicate.kt @@ -4,7 +4,7 @@ package moe.nea.firmament.features.texturepack.predicates import com.google.gson.JsonElement import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack object AlwaysPredicate : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt index 99abaaa..c2ed24f 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt @@ -3,15 +3,16 @@ package moe.nea.firmament.features.texturepack.predicates import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject +import net.minecraft.world.entity.LivingEntity import moe.nea.firmament.features.texturepack.CustomModelOverrideParser import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { - override fun test(stack: ItemStack): Boolean { - return children.all { it.test(stack) } - } + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return children.all { it.test(stack, holder) } + } object Parser : FirmamentModelPredicateParser { override fun parse(jsonElement: JsonElement): FirmamentModelPredicate { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt index 7ccaadf..b54dd5d 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt @@ -1,10 +1,11 @@ package moe.nea.firmament.features.texturepack.predicates import com.google.gson.JsonElement -import net.minecraft.item.ItemStack +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import moe.nea.firmament.util.MC class CastPredicate : FirmamentModelPredicate { object Parser : FirmamentModelPredicateParser { @@ -14,7 +15,11 @@ class CastPredicate : FirmamentModelPredicate { } } + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + return (holder as? Player)?.fishing != null && holder.mainHandItem === stack + } + override fun test(stack: ItemStack): Boolean { - return MC.player?.fishHook != null // TODO pass through more of the model predicate context + return false } } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt index 04c7a2b..0d4fb84 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/DisplayNamePredicate.kt @@ -5,7 +5,7 @@ import com.google.gson.JsonElement import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import moe.nea.firmament.features.texturepack.StringMatcher -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack import moe.nea.firmament.util.mc.displayNameAccordingToNbt data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt index 3c8023d..e94f5c6 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt @@ -1,215 +1,220 @@ package moe.nea.firmament.features.texturepack.predicates -import com.google.gson.JsonArray import com.google.gson.JsonElement import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import kotlin.jvm.optionals.getOrDefault +import kotlin.jvm.optionals.getOrNull import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import moe.nea.firmament.features.texturepack.StringMatcher -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 net.minecraft.world.item.ItemStack +import net.minecraft.nbt.ByteTag +import net.minecraft.nbt.DoubleTag +import net.minecraft.nbt.Tag +import net.minecraft.nbt.FloatTag +import net.minecraft.nbt.IntTag +import net.minecraft.nbt.LongTag +import net.minecraft.nbt.ShortTag import moe.nea.firmament.util.extraAttributes +import moe.nea.firmament.util.mc.NbtPrism 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( + fun matches(nbt: Tag): 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? IntTag)?.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? FloatTag)?.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? DoubleTag)?.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? LongTag)?.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? ShortTag)?.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? ByteTag)?.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 nbtExtractor: (Tag) -> 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)" - } - } + ): 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: Tag): Boolean { + return when (nbt) { + is ByteTag -> nbt.byteValue().toLong() == number + is IntTag -> nbt.intValue().toLong() == number + is ShortTag -> nbt.shortValue().toLong() == number + is LongTag -> nbt.longValue().toLong() == number + else -> false + } + } + + } + + class MatchStringExact(val string: String) : NbtMatcher { + override fun matches(nbt: Tag): Boolean { + return nbt.asString().getOrNull() == string + } + + override fun toString(): String { + return "MatchNbtStringExactly($string)" + } + } + + class MatchString(val string: StringMatcher) : NbtMatcher { + override fun matches(nbt: Tag): Boolean { + return nbt.asString().map(string::matches).getOrDefault(false) + } + + override fun toString(): String { + return "MatchNbtString($string)" + } + } } data class ExtraAttributesPredicate( @@ -217,55 +222,20 @@ data class ExtraAttributesPredicate( 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) } - } + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + return ExtraAttributesPredicate(prism, 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/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt new file mode 100644 index 0000000..4b0b7cb --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt @@ -0,0 +1,59 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.mojang.serialization.Codec +import kotlin.jvm.optionals.getOrNull +import net.minecraft.core.component.DataComponentType +import net.minecraft.world.item.component.CustomData +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.nbt.NbtOps +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.mc.NbtPrism +import moe.nea.firmament.util.mc.unsafeNbt + +data class GenericComponentPredicate<T>( + val componentType: DataComponentType<T>, + val codec: Codec<T>, + val path: NbtPrism, + val matcher: NbtMatcher, +) : FirmamentModelPredicate { + constructor(componentType: DataComponentType<T>, path: NbtPrism, matcher: NbtMatcher) + : this(componentType, componentType.codecOrThrow(), path, matcher) + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + val component = stack.get(componentType) ?: return false + // TODO: cache this + val nbt = + if (component is CustomData) component.unsafeNbt + else codec.encodeStart(NbtOps.INSTANCE, component) + .resultOrPartial().getOrNull() ?: return false + return path.access(nbt).any { matcher.matches(it) } + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): GenericComponentPredicate<*>? { + if (jsonElement !is JsonObject) return null + val path = jsonElement.get("path") ?: return null + val prism = NbtPrism.fromElement(path) ?: return null + val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement) + ?: return null + val component = MC.currentOrDefaultRegistries + .lookupOrThrow(Registries.DATA_COMPONENT_TYPE) + .getOrThrow( + ResourceKey.create( + Registries.DATA_COMPONENT_TYPE, + ResourceLocation.parse(jsonElement.get("component").asString) + ) + ).value() + return GenericComponentPredicate(component, prism, matcher) + } + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt index 3cb80c7..e90d9fb 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt @@ -6,27 +6,27 @@ import com.google.gson.JsonPrimitive import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser 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 net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.resources.ResourceKey +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceLocation import moe.nea.firmament.util.MC class ItemPredicate( val item: Item ) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { - return stack.item == item + return stack.`is`(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) + val itemKey = ResourceKey.create(Registries.ITEM, + ResourceLocation.tryParse(jsonElement.asString) ?: return null) - return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null) + return ItemPredicate(MC.defaultItems.get(itemKey).getOrNull()?.value() ?: return null) } return null } diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt index f0b4737..6b2a8b9 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/LorePredicate.kt @@ -5,7 +5,7 @@ import com.google.gson.JsonElement import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import moe.nea.firmament.features.texturepack.StringMatcher -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack import moe.nea.firmament.util.mc.loreAccordingToNbt class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt index 4986ad9..d0ad11c 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/NotPredicate.kt @@ -6,7 +6,7 @@ import com.google.gson.JsonObject import moe.nea.firmament.features.texturepack.CustomModelOverrideParser import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt index e3093cd..c32543f 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/OrPredicate.kt @@ -7,7 +7,7 @@ import com.google.gson.JsonObject import moe.nea.firmament.features.texturepack.CustomModelOverrideParser import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate { override fun test(stack: ItemStack): Boolean { diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt index b30b7c9..9de51d2 100644 --- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PetPredicate.kt @@ -7,7 +7,7 @@ import moe.nea.firmament.features.texturepack.FirmamentModelPredicate import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser import moe.nea.firmament.features.texturepack.RarityMatcher import moe.nea.firmament.features.texturepack.StringMatcher -import net.minecraft.item.ItemStack +import net.minecraft.world.item.ItemStack import moe.nea.firmament.repo.ExpLadders import moe.nea.firmament.util.petData diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt new file mode 100644 index 0000000..330fe88 --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt @@ -0,0 +1,26 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.BowItem +import net.minecraft.world.item.ItemStack +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser + +class PullingPredicate(val percentage: Double) : FirmamentModelPredicate { + companion object { + val AnyPulling = PullingPredicate(0.1) + } + + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + return PullingPredicate(jsonElement.asDouble) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (holder == null) return false + return BowItem.getPowerForTime(holder.ticksUsingItem) >= percentage + } + +} diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt new file mode 100644 index 0000000..cbef02e --- /dev/null +++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt @@ -0,0 +1,63 @@ +package moe.nea.firmament.features.texturepack.predicates + +import com.google.gson.JsonElement +import com.mojang.authlib.minecraft.MinecraftProfileTexture +import java.util.UUID +import kotlin.jvm.optionals.getOrNull +import net.minecraft.core.component.DataComponents +import net.minecraft.world.entity.LivingEntity +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import moe.nea.firmament.features.texturepack.FirmamentModelPredicate +import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser +import moe.nea.firmament.features.texturepack.StringMatcher +import moe.nea.firmament.util.mc.decodeProfileTextureProperty +import moe.nea.firmament.util.parsePotentiallyDashlessUUID + +class SkullPredicate( + val profileId: UUID?, + val textureProfileId: UUID?, + val skinUrl: StringMatcher?, + val textureValue: StringMatcher?, +) : FirmamentModelPredicate { + object Parser : FirmamentModelPredicateParser { + override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? { + val obj = jsonElement.asJsonObject + val profileId = obj.getAsJsonPrimitive("profileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureProfileId = obj.getAsJsonPrimitive("textureProfileId") + ?.asString?.let(::parsePotentiallyDashlessUUID) + val textureValue = obj.get("textureValue")?.let(StringMatcher::parse) + val skinUrl = obj.get("skinUrl")?.let(StringMatcher::parse) + return SkullPredicate(profileId, textureProfileId, skinUrl, textureValue) + } + } + + override fun test(stack: ItemStack, holder: LivingEntity?): Boolean { + if (!stack.`is`(Items.PLAYER_HEAD)) return false + val profile = stack.get(DataComponents.PROFILE) ?: return false + val textureProperty = profile.partialProfile().properties["textures"].firstOrNull() + val textureMode = lazy(LazyThreadSafetyMode.NONE) { + decodeProfileTextureProperty(textureProperty ?: return@lazy null) + } + when { + profileId != null + && profileId != profile.partialProfile().id -> + return false + + textureValue != null + && !textureValue.matches(textureProperty?.value ?: "") -> + return false + + skinUrl != null + && !skinUrl.matches(textureMode.value?.textures?.get(MinecraftProfileTexture.Type.SKIN)?.url ?: "") -> + return false + + textureProfileId != null + && textureProfileId != textureMode.value?.profileId -> + return false + + else -> return true + } + } +} |
