From 7682534f6fd139b75f24c79b76098fe05f0fa0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Linnea=20Gr=C3=A4f?= Date: Sat, 11 May 2024 03:28:05 +0200 Subject: [PATCH] Add custom global textures --- docs/Texture Pack Format.md | 46 +++++ .../mixins/CustomModelBakerPatch.java | 9 +- .../mixins/EarlyResourceReloadPatch.java | 30 ++++ .../ResourceReloaderRegistrationPatch.java | 31 ++++ .../GlobalModelOverridePatch.java | 34 ++++ .../kotlin/moe/nea/firmament/Firmament.kt | 26 ++- .../firmament/events/BakeExtraModelsEvent.kt | 21 +++ .../firmament/events/ClientStartedEvent.kt | 11 ++ .../events/EarlyResourceReloadEvent.kt | 15 ++ .../events/FinalizeResourceManagerEvent.kt | 15 ++ .../texturepack/CustomGlobalTextures.kt | 161 ++++++++++++++++++ .../texturepack/CustomModelOverrideParser.kt | 18 ++ .../texturepack/CustomSkyBlockTextures.kt | 18 ++ .../features/texturepack/StringMatcher.kt | 85 +++++++++ .../firmament/util/IdentifierSerializer.kt | 28 +++ .../moe/nea/firmament/util/LogIfNull.kt | 13 ++ .../firmament/filters/screen/always.json | 5 + .../filters/screen/always.json.license | 3 + src/main/resources/assets/firmament/icon.png | Bin 20279 -> 0 bytes .../assets/firmament/icon.png.license | 3 - 20 files changed, 556 insertions(+), 16 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java create mode 100644 src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/GlobalModelOverridePatch.java create mode 100644 src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt create mode 100644 src/main/kotlin/moe/nea/firmament/events/ClientStartedEvent.kt create mode 100644 src/main/kotlin/moe/nea/firmament/events/EarlyResourceReloadEvent.kt create mode 100644 src/main/kotlin/moe/nea/firmament/events/FinalizeResourceManagerEvent.kt create mode 100644 src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt create mode 100644 src/main/kotlin/moe/nea/firmament/util/IdentifierSerializer.kt create mode 100644 src/main/kotlin/moe/nea/firmament/util/LogIfNull.kt create mode 100644 src/main/resources/assets/firmament/filters/screen/always.json create mode 100644 src/main/resources/assets/firmament/filters/screen/always.json.license delete mode 100644 src/main/resources/assets/firmament/icon.png delete mode 100644 src/main/resources/assets/firmament/icon.png.license diff --git a/docs/Texture Pack Format.md b/docs/Texture Pack Format.md index 5e466bf..bebcbb4 100644 --- a/docs/Texture Pack Format.md +++ b/docs/Texture Pack Format.md @@ -157,3 +157,49 @@ armor you can use `assets/firmskyblock/textures/models/armor/{skyblock_id}_layer regular leather colors. If you want the leather colors to be applied, supply the normal non-`_overlay` variant, and also supply a blank `_overlay` variant. You can also put non-color-affected parts into the `_overlay` variant. +## Global Item Texture Replacement + +Most texture replacement is done based on the SkyBlock id of the item. However, some items you might want to re-texture +do not have an id. The next best alternative you had before was just to replace the vanilla item and add a bunch of +predicates. This tries to fix this problem, at the cost of being more performance intensive than the other re-texturing +methods. + +The entrypoint to global overrides is `firmskyblock:overrides/item`. Put your overrides into that folder, with one file +per override. + +```json5 +{ + "screen": "testrp:chocolate_factory", + "model": "testrp:time_tower", + "predicate": { + "firmament:display_name": { + "regex": "Time Tower.*" + } + } +} +``` + +There are three parts to the override. + +The `model` is an *item id* that the item will be replaced with. This means the +model will be loaded from `assets//models/item/.json`. Make sure to use your own namespace to +avoid collisions with other texture packs that might use the same id for a gui. + +The `predicate` is just a normal [predicate](#predicates). This one does not support the vanilla predicates. You can +still use vanilla predicates in the resolved model, but this will not allow you to fall back to other global overrides. + +### Global item texture Screens + +In order to improve performance not all overrides are tested all the time. Instead you can prefilter by the screen that +is open. First the gui is resolved to `assets//filters/screen/.json`. Make sure to use your own namespace +to avoid collisions with other texture packs that might use the same id for a screen. + +```json +{ + "title": "Chocolate Factory" +} +``` + +Currently, the only supported filter is `title`, which accepts a [string matcher](#string-matcher). You can also use +`firmament:always` as an always on filter (this is the recommended way). + diff --git a/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java b/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java index 2da4176..6bd9ba1 100644 --- a/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java @@ -1,11 +1,13 @@ /* * SPDX-FileCopyrightText: 2023 Linnea Gräf + * SPDX-FileCopyrightText: 2024 Linnea Gräf * * SPDX-License-Identifier: GPL-3.0-or-later */ package moe.nea.firmament.mixins; +import moe.nea.firmament.events.BakeExtraModelsEvent; import net.minecraft.client.MinecraftClient; import net.minecraft.client.render.model.ModelLoader; import net.minecraft.client.render.model.UnbakedModel; @@ -39,12 +41,7 @@ public abstract class CustomModelBakerPatch { @Inject(method = "bake", at = @At("HEAD")) public void onBake(BiFunction spriteLoader, CallbackInfo ci) { - Map resources = - MinecraftClient.getInstance().getResourceManager().findResources("models/item", it -> "firmskyblock".equals(it.getNamespace()) && it.getPath().endsWith(".json")); - for (Identifier identifier : resources.keySet()) { - ModelIdentifier modelId = new ModelIdentifier("firmskyblock", identifier.getPath().substring("models/item/".length(), identifier.getPath().length() - ".json".length()), "inventory"); - addModel(modelId); - } + BakeExtraModelsEvent.Companion.publish(new BakeExtraModelsEvent(this::addModel)); modelsToBake.values().forEach(model -> model.setParents(this::getOrLoadModel)); } } diff --git a/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java b/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java new file mode 100644 index 0000000..144eda1 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/EarlyResourceReloadPatch.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.EarlyResourceReloadEvent; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.ResourceReload; +import net.minecraft.util.Unit; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +@Mixin(ReloadableResourceManagerImpl.class) +public abstract class EarlyResourceReloadPatch implements ResourceManager { + @Inject(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/SimpleResourceReload;start(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Ljava/util/concurrent/Executor;Ljava/util/concurrent/Executor;Ljava/util/concurrent/CompletableFuture;Z)Lnet/minecraft/resource/ResourceReload;", shift = At.Shift.BEFORE)) + public void onResourceReload(Executor prepareExecutor, Executor applyExecutor, CompletableFuture initialStage, List packs, CallbackInfoReturnable cir) { + EarlyResourceReloadEvent.Companion.publish(new EarlyResourceReloadEvent(this, prepareExecutor)); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java new file mode 100644 index 0000000..bf0b925 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/ResourceReloaderRegistrationPatch.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.mixins; + +import moe.nea.firmament.events.FinalizeResourceManagerEvent; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.RunArgs; +import net.minecraft.resource.ReloadableResourceManagerImpl; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MinecraftClient.class) +public class ResourceReloaderRegistrationPatch { + @Shadow + @Final + private ReloadableResourceManagerImpl resourceManager; + + @Inject(method = "", at = @At(value = "INVOKE", target = "Lnet/minecraft/resource/ResourcePackManager;createResourcePacks()Ljava/util/List;", shift = At.Shift.BEFORE)) + private void onBeforeResourcePackCreation(RunArgs args, CallbackInfo ci) { + FinalizeResourceManagerEvent.Companion.publish(new FinalizeResourceManagerEvent(this.resourceManager)); + } +} + diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/GlobalModelOverridePatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/GlobalModelOverridePatch.java new file mode 100644 index 0000000..ec1d09c --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/GlobalModelOverridePatch.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.CustomGlobalTextures; +import net.minecraft.client.render.item.ItemModels; +import net.minecraft.client.render.item.ItemRenderer; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.entity.LivingEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.world.World; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ItemRenderer.class) +public abstract class GlobalModelOverridePatch { + + @Shadow + public abstract ItemModels getModels(); + + @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) + private void overrideGlobalModel( + ItemStack stack, World world, LivingEntity entity, + int seed, CallbackInfoReturnable cir) { + CustomGlobalTextures.replaceGlobalModel(this.getModels(), stack, cir); + } +} diff --git a/src/main/kotlin/moe/nea/firmament/Firmament.kt b/src/main/kotlin/moe/nea/firmament/Firmament.kt index 03d4576..630950a 100644 --- a/src/main/kotlin/moe/nea/firmament/Firmament.kt +++ b/src/main/kotlin/moe/nea/firmament/Firmament.kt @@ -8,13 +8,15 @@ package moe.nea.firmament import com.mojang.brigadier.CommandDispatcher -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.cache.* -import io.ktor.client.plugins.compression.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* +import io.ktor.client.HttpClient +import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.cache.HttpCache +import io.ktor.client.plugins.compression.ContentEncoding +import io.ktor.client.plugins.contentnegotiation.ContentNegotiation +import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logging +import io.ktor.serialization.kotlinx.json.json +import java.io.InputStream import java.nio.file.Files import java.nio.file.Path import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback @@ -36,11 +38,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.plus import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream import kotlin.coroutines.EmptyCoroutineContext import net.minecraft.command.CommandRegistryAccess import net.minecraft.util.Identifier import moe.nea.firmament.commands.registerFirmamentCommand import moe.nea.firmament.dbus.FirmamentDbusObject +import moe.nea.firmament.events.ClientStartedEvent import moe.nea.firmament.events.CommandEvent import moe.nea.firmament.events.ItemTooltipEvent import moe.nea.firmament.events.ScreenRenderPostEvent @@ -133,6 +137,9 @@ object Firmament { FeatureManager.autoload() HypixelStaticData.spawnDataCollectionLoop() ClientCommandRegistrationCallback.EVENT.register(this::registerCommands) + ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted { + ClientStartedEvent.publish(ClientStartedEvent()) + }) ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping { logger.info("Shutting down Firmament coroutines") globalJob.cancel() @@ -151,4 +158,9 @@ object Firmament { fun identifier(path: String) = Identifier(MOD_ID, path) + inline fun tryDecodeJsonFromStream(inputStream: InputStream): Result { + return runCatching { + json.decodeFromStream(inputStream) + } + } } diff --git a/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt b/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt new file mode 100644 index 0000000..7b74166 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.events + +import java.util.function.Consumer +import net.minecraft.client.util.ModelIdentifier + +class BakeExtraModelsEvent( + private val addModel: Consumer, +) : FirmamentEvent() { + + fun addModel(modelIdentifier: ModelIdentifier) { + this.addModel.accept(modelIdentifier) + } + + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/moe/nea/firmament/events/ClientStartedEvent.kt b/src/main/kotlin/moe/nea/firmament/events/ClientStartedEvent.kt new file mode 100644 index 0000000..151e12d --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/ClientStartedEvent.kt @@ -0,0 +1,11 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.events + +class ClientStartedEvent : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/moe/nea/firmament/events/EarlyResourceReloadEvent.kt b/src/main/kotlin/moe/nea/firmament/events/EarlyResourceReloadEvent.kt new file mode 100644 index 0000000..9013aa4 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/EarlyResourceReloadEvent.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.events + +import java.util.concurrent.Executor +import net.minecraft.resource.ResourceManager + +data class EarlyResourceReloadEvent(val resourceManager: ResourceManager, val preparationExecutor: Executor) : + FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/moe/nea/firmament/events/FinalizeResourceManagerEvent.kt b/src/main/kotlin/moe/nea/firmament/events/FinalizeResourceManagerEvent.kt new file mode 100644 index 0000000..36b2498 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/events/FinalizeResourceManagerEvent.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.events + +import net.minecraft.resource.ReloadableResourceManagerImpl + +data class FinalizeResourceManagerEvent( + val resourceManager: ReloadableResourceManagerImpl, +) : FirmamentEvent() { + companion object : FirmamentEventBus() +} diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt new file mode 100644 index 0000000..2eb4ee1 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -0,0 +1,161 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class) + +package moe.nea.firmament.features.texturepack + + +import java.util.concurrent.CompletableFuture +import org.slf4j.LoggerFactory +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable +import kotlinx.serialization.Serializable +import kotlinx.serialization.UseSerializers +import kotlin.jvm.optionals.getOrNull +import net.minecraft.client.render.item.ItemModels +import net.minecraft.client.render.model.BakedModel +import net.minecraft.client.util.ModelIdentifier +import net.minecraft.item.ItemStack +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.util.Identifier +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.BakeExtraModelsEvent +import moe.nea.firmament.events.EarlyResourceReloadEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.ScreenChangeEvent +import moe.nea.firmament.events.subscription.SubscriptionOwner +import moe.nea.firmament.features.FirmamentFeature +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.runNull + +object CustomGlobalTextures : SinglePreparationResourceReloader(), + SubscriptionOwner { + override val delegateFeature: FirmamentFeature + get() = CustomSkyBlockTextures + + class CustomGuiTextureOverride( + val classes: List + ) + + @Serializable + data class GlobalItemOverride( + val screen: Identifier, + val model: Identifier, + val predicate: FirmamentModelPredicate, + ) + + @Serializable + data class ScreenFilter( + val title: StringMatcher, + ) + + data class ItemOverrideCollection( + val screenFilter: ScreenFilter, + val overrides: List, + ) + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + MC.resourceManager.registerReloader(this) + } + + @Subscribe + fun onEarlyReload(event: EarlyResourceReloadEvent) { + preparationFuture = CompletableFuture + .supplyAsync( + { + prepare(event.resourceManager) + }, event.preparationExecutor) + } + + @Subscribe + fun onBakeModels(event: BakeExtraModelsEvent) { + for (guiClassOverride in preparationFuture.join().classes) { + for (override in guiClassOverride.overrides) { + event.addModel(ModelIdentifier(override.model, "inventory")) + } + } + } + + @Volatile + var preparationFuture: CompletableFuture = CompletableFuture.completedFuture( + CustomGuiTextureOverride(listOf())) + + override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride { + return preparationFuture.join() + } + + override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) { + this.guiClassOverrides = prepared + } + + val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java) + fun prepare(manager: ResourceManager): CustomGuiTextureOverride { + val overrideResources = + manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") } + .mapNotNull { + Firmament.tryDecodeJsonFromStream(it.value.inputStream).getOrElse { ex -> + logger.error("Failed to load global item override at ${it.key}", ex) + null + } + } + + val byGuiClass = overrideResources.groupBy { it.screen } + val guiClasses = byGuiClass.entries + .mapNotNull { + val key = it.key + val guiClassResource = + manager.getResource(Identifier(key.namespace, "filters/screen/${key.path}.json")) + .getOrNull() + ?: return@mapNotNull runNull { + logger.error("Failed to locate screen filter at $key") + } + val screenFilter = + Firmament.tryDecodeJsonFromStream(guiClassResource.inputStream) + .getOrElse { ex -> + logger.error("Failed to load screen filter at $key", ex) + return@mapNotNull null + } + ItemOverrideCollection(screenFilter, it.value) + } + logger.info("Loaded ${overrideResources.size} global item overrides") + return CustomGuiTextureOverride(guiClasses) + } + + var guiClassOverrides = CustomGuiTextureOverride(listOf()) + + var matchingOverrides: List = listOf() + + @Subscribe + fun onOpenGui(event: ScreenChangeEvent) { + val newTitle = event.new?.title + matchingOverrides = + if (newTitle == null) listOf() + else guiClassOverrides.classes.filter { it.screenFilter.title.matches(newTitle) } + } + + @JvmStatic + fun replaceGlobalModel( + models: ItemModels, + stack: ItemStack, + cir: CallbackInfoReturnable + ) { + for (guiClassOverride in matchingOverrides) { + for (override in guiClassOverride.overrides) { + if (override.predicate.test(stack)) { + cir.returnValue = models.modelManager.getModel(ModelIdentifier(override.model, "inventory")) + return + } + } + } + } + + +} diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt index ad1f436..d512dec 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt @@ -7,9 +7,27 @@ package moe.nea.firmament.features.texturepack import com.google.gson.JsonObject +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import net.minecraft.util.Identifier object CustomModelOverrideParser { + object FirmamentRootPredicateSerializer : KSerializer { + val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): FirmamentModelPredicate { + val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject + return AndPredicate(parsePredicates(json).toTypedArray()) + } + + override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) { + TODO("Cannot serialize firmament predicates") + } + } val predicateParsers = mutableMapOf() diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt index e66a24a..7c2d5ab 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt @@ -17,6 +17,7 @@ import net.minecraft.client.util.ModelIdentifier import net.minecraft.component.type.ProfileComponent import net.minecraft.util.Identifier import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.BakeExtraModelsEvent import moe.nea.firmament.events.CustomItemModelEvent import moe.nea.firmament.events.TickEvent import moe.nea.firmament.features.FirmamentFeature @@ -47,6 +48,23 @@ object CustomSkyBlockTextures : FirmamentFeature { } } + @Subscribe + fun bakeCustomFirmModels(event: BakeExtraModelsEvent) { + val resources = + MinecraftClient.getInstance().resourceManager.findResources("models/item" + ) { it: Identifier -> + "firmskyblock" == it.namespace && it.path + .endsWith(".json") + } + for (identifier in resources.keys) { + val modelId = ModelIdentifier("firmskyblock", + identifier.path.substring("models/item/".length, + identifier.path.length - ".json".length), + "inventory") + event.addModel(modelId) + } + } + @Subscribe fun onCustomModelId(it: CustomItemModelEvent) { if (!TConfig.enabled) return diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt index 8c1ccbc..1c2675f 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/StringMatcher.kt @@ -6,15 +6,24 @@ package moe.nea.firmament.features.texturepack +import com.google.gson.JsonArray import com.google.gson.JsonElement +import com.google.gson.JsonNull import com.google.gson.JsonObject import com.google.gson.JsonPrimitive +import com.google.gson.internal.LazilyParsedNumber import java.util.function.Predicate +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import net.minecraft.nbt.NbtString import net.minecraft.text.Text import moe.nea.firmament.util.MC import moe.nea.firmament.util.removeColorCodes +@Serializable(with = StringMatcher.Serializer::class) interface StringMatcher { fun matches(string: String): Boolean fun matches(text: Text): Boolean { @@ -46,7 +55,28 @@ interface StringMatcher { } } + object Serializer : KSerializer { + val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): StringMatcher { + val delegate = decoder.decodeSerializableValue(delegateSerializer) + val gsonDelegate = delegate.intoGson() + return parse(gsonDelegate) + } + + override fun serialize(encoder: Encoder, value: StringMatcher) { + encoder.encodeSerializableValue(delegateSerializer, Companion.serialize(value).intoKotlinJson()) + } + + } + companion object { + fun serialize(stringMatcher: StringMatcher):JsonElement { + TODO("Cannot serialize string matchers rn") + } + fun parse(jsonElement: JsonElement): StringMatcher { if (jsonElement is JsonPrimitive) { return Equals(jsonElement.asString, true) @@ -69,3 +99,58 @@ interface StringMatcher { } } } + +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 + } + } +} diff --git a/src/main/kotlin/moe/nea/firmament/util/IdentifierSerializer.kt b/src/main/kotlin/moe/nea/firmament/util/IdentifierSerializer.kt new file mode 100644 index 0000000..3c1aa52 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/util/IdentifierSerializer.kt @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.util + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.minecraft.util.Identifier + +object IdentifierSerializer : KSerializer { + val delegateSerializer = String.serializer() + override val descriptor: SerialDescriptor + get() = SerialDescriptor("Identifier", delegateSerializer.descriptor) + + override fun deserialize(decoder: Decoder): Identifier { + return Identifier(decoder.decodeSerializableValue(delegateSerializer)) + } + + override fun serialize(encoder: Encoder, value: Identifier) { + encoder.encodeSerializableValue(delegateSerializer, value.toString()) + } +} diff --git a/src/main/kotlin/moe/nea/firmament/util/LogIfNull.kt b/src/main/kotlin/moe/nea/firmament/util/LogIfNull.kt new file mode 100644 index 0000000..b0476ee --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/util/LogIfNull.kt @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2024 Linnea Gräf + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package moe.nea.firmament.util + + +fun runNull(block: () -> Unit): Nothing? { + block() + return null +} diff --git a/src/main/resources/assets/firmament/filters/screen/always.json b/src/main/resources/assets/firmament/filters/screen/always.json new file mode 100644 index 0000000..6c21cc9 --- /dev/null +++ b/src/main/resources/assets/firmament/filters/screen/always.json @@ -0,0 +1,5 @@ +{ + "title": { + "regex": ".*" + } +} diff --git a/src/main/resources/assets/firmament/filters/screen/always.json.license b/src/main/resources/assets/firmament/filters/screen/always.json.license new file mode 100644 index 0000000..5f0659f --- /dev/null +++ b/src/main/resources/assets/firmament/filters/screen/always.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2024 Linnea Gräf + +SPDX-License-Identifier: CC0-1.0 diff --git a/src/main/resources/assets/firmament/icon.png b/src/main/resources/assets/firmament/icon.png deleted file mode 100644 index 467f962838f7a0ba5d3521439f998c4afa47c3d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20279 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 diff --git a/src/main/resources/assets/firmament/icon.png.license b/src/main/resources/assets/firmament/icon.png.license deleted file mode 100644 index f168dcf..0000000 --- a/src/main/resources/assets/firmament/icon.png.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2023 Linnea Gräf - -SPDX-License-Identifier: CC-BY-4.0