From 1606188d9ad65c66e9d873497ea3271dbdadaf77 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Fri, 9 Aug 2024 00:49:36 +0200 Subject: Add custom block textures --- build.gradle.kts | 1 + docs/Texture Pack Format.md | 51 ++++ .../firmament/mixins/CustomModelBakerPatch.java | 15 +- .../PatchBlockModelInSodiumChunkGenerator.java | 29 +++ .../custommodels/ReplaceBlockBreakSoundPatch.java | 27 ++ .../custommodels/ReplaceBlockHitSoundPatch.java | 30 +++ .../ReplaceBlockRenderManagerBlockModel.java | 38 +++ .../custommodels/ReplaceFallbackBlockModel.java | 21 ++ .../nea/firmament/events/BakeExtraModelsEvent.kt | 11 +- .../features/texturepack/CustomBlockTextures.kt | 277 +++++++++++++++++++++ .../features/texturepack/CustomGlobalTextures.kt | 2 +- .../features/texturepack/CustomSkyBlockTextures.kt | 5 +- .../moe/nea/firmament/util/IdentifierSerializer.kt | 4 +- src/main/kotlin/moe/nea/firmament/util/MC.kt | 13 +- .../nea/firmament/util/json/BlockPosSerializer.kt | 25 ++ .../resources/assets/firmament/lang/en_us.json | 1 + 16 files changed, 537 insertions(+), 13 deletions(-) create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java create mode 100644 src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java create mode 100644 src/main/kotlin/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt create mode 100644 src/main/kotlin/moe/nea/firmament/util/json/BlockPosSerializer.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6e766fa..24b263e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -139,6 +139,7 @@ dependencies { modRuntimeOnly(libs.fabric.api.deprecated) modApi(libs.architectury) modCompileOnly(libs.jarvis.api) + modCompileOnly(libs.sodium) include(libs.jarvis.fabric) modCompileOnly(libs.femalegender) diff --git a/docs/Texture Pack Format.md b/docs/Texture Pack Format.md index 859252f..4726e53 100644 --- a/docs/Texture Pack Format.md +++ b/docs/Texture Pack Format.md @@ -465,3 +465,54 @@ to avoid collisions with other texture packs that might use the same id for a sc 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). +## Block Model Replacements + +If you want to replace block textures in the world you can do so using block overrides. Those are stored in +`assets/firmskyblock/overrides/blocks/.json`. The id does not matter, all overrides are loaded. This file specifies +which block models are replaced under which conditions: + +```json +{ + "modes": [ + "mining_3" + ], + "area": [ + { + "min": [ + -31, + 200, + -117 + ], + "max": [ + 12, + 223, + -95 + ] + } + ], + "replacements": { + "minecraft:blue_wool": "firmskyblock:mithril_deep", + "minecraft:light_blue_wool": { + "block": "firmskyblock:mithril_deep", + "sound": "minecraft:block.wet_sponge.hit" + } + } +} +``` + +| Field | Required | Description | +|-------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `modes` | yes | A list of `/locraw` mode names. | +| `area` | no | A list of areas. Blocks outside of the coordinate range will be ignored. If the block is in *any* range it will be considered inside | +| `area.min` | yes | The lowest coordinate in the area. Is included in the area. | +| `area.max` | yes | The highest coordinate in the area. Is included in the area. | +| `replacements` | yes | A map of block id to replacement mappings | +| `replacements` (string) | yes | You can directly specify a string. Equivalent to just setting `replacements.block`. | +| `replacements.block` | yes | You can specify a block model to be used instead of the regular one. The model will be loaded from `assets//models/block/.json` like regular block models. | +| `replacements.sound` | no | You can also specify a sound override. This is only used for the "hit" sound effect that repeats while the block is mined. The "break" sound effect played after a block was finished mining is sadly sent by hypixel directly and cannot be replaced reliably. | + +> A quick note about optimization: Not specifying an area (by just omitting the `area` field) is quicker than having an +> area encompass the entire map. +> +> If you need to use multiple `area`s for unrelated sections of the world it might be a performance improvement to move +> unrelated models to different files to reduce the amount of area checks being done for each block. diff --git a/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java b/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java index 97f81b0..c1e359d 100644 --- a/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java +++ b/src/main/java/moe/nea/firmament/mixins/CustomModelBakerPatch.java @@ -1,5 +1,3 @@ - - package moe.nea.firmament.mixins; import moe.nea.firmament.events.BakeExtraModelsEvent; @@ -10,6 +8,7 @@ import net.minecraft.util.Identifier; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -29,9 +28,19 @@ public abstract class CustomModelBakerPatch { @Shadow abstract UnbakedModel getOrLoadModel(Identifier id); + @Shadow + protected abstract void add(ModelIdentifier id, UnbakedModel model); + + @Unique + private void loadNonItemModel(ModelIdentifier identifier) { + UnbakedModel unbakedModel = this.getOrLoadModel(identifier.id()); + this.add(identifier, unbakedModel); + } + + @Inject(method = "bake", at = @At("HEAD")) public void onBake(ModelLoader.SpriteGetter spliteGetter, CallbackInfo ci) { - BakeExtraModelsEvent.Companion.publish(new BakeExtraModelsEvent(this::loadItemModel)); + BakeExtraModelsEvent.Companion.publish(new BakeExtraModelsEvent(this::loadItemModel, this::loadNonItemModel)); modelsToBake.values().forEach(model -> model.setParents(this::getOrLoadModel)); // modelsToBake.keySet().stream() // .filter(it -> !it.id().getNamespace().equals("minecraft")) diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java b/src/main/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java new file mode 100644 index 0000000..90f20bc --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/PatchBlockModelInSodiumChunkGenerator.java @@ -0,0 +1,29 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import me.jellysquid.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockModels; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ChunkBuilderMeshingTask.class) +public class PatchBlockModelInSodiumChunkGenerator { + @WrapOperation( + method = "execute(Lme/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildContext;Lme/jellysquid/mods/sodium/client/util/task/CancellationToken;)Lme/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuildOutput;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) + private BakedModel replaceBlockModel(BlockModels instance, BlockState state, Operation original, + @Local(name = "blockPos") BlockPos.Mutable pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java new file mode 100644 index 0000000..9401889 --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockBreakSoundPatch.java @@ -0,0 +1,27 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.WorldRenderer; +import net.minecraft.sound.BlockSoundGroup; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(WorldRenderer.class) +public class ReplaceBlockBreakSoundPatch { +// Sadly hypixel does not send a world event here and instead plays the sound on the server directly +// @WrapOperation(method = "processWorldEvent", at = @At(value = "INVOKE", target = "Lnet/minecraft/sound/BlockSoundGroup;getBreakSound()Lnet/minecraft/sound/SoundEvent;")) +// private SoundEvent replaceBreakSoundEvent(BlockSoundGroup instance, Operation original, +// @Local(argsOnly = true) BlockPos pos, @Local BlockState blockState) { +// var replacement = CustomBlockTextures.getReplacement(blockState, pos); +// if (replacement != null && replacement.getSound() != null) { +// return SoundEvent.of(replacement.getSound()); +// } +// return original.call(instance); +// } +} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java new file mode 100644 index 0000000..f9a1d0d --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java @@ -0,0 +1,30 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.client.sound.PositionedSoundInstance; +import net.minecraft.sound.SoundCategory; +import net.minecraft.sound.SoundEvent; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.random.Random; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(ClientPlayerInteractionManager.class) +public class ReplaceBlockHitSoundPatch { + @WrapOperation(method = "updateBlockBreakingProgress", at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;")) + private PositionedSoundInstance replaceSound( + SoundEvent sound, SoundCategory category, float volume, float pitch, + Random random, BlockPos pos, Operation original, + @Local BlockState blockState) { + var replacement = CustomBlockTextures.getReplacement(blockState, pos); + if (replacement != null && replacement.getSound() != null) { + sound = SoundEvent.of(replacement.getSound()); + } + return original.call(sound, category, volume, pitch, random, pos); + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java new file mode 100644 index 0000000..711b2af --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java @@ -0,0 +1,38 @@ +package moe.nea.firmament.mixins.custommodels; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockModels; +import net.minecraft.client.render.block.BlockRenderManager; +import net.minecraft.client.render.model.BakedModel; +import net.minecraft.util.math.BlockPos; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(BlockRenderManager.class) +public class ReplaceBlockRenderManagerBlockModel { + @WrapOperation(method = "renderBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) + private BakedModel replaceModelInRenderBlock( + BlockRenderManager instance, BlockState state, Operation original, @Local(argsOnly = true) BlockPos pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } + + @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;")) + private BakedModel replaceModelInRenderDamage( + BlockModels instance, BlockState state, Operation original, @Local(argsOnly = true) BlockPos pos) { + var replacement = CustomBlockTextures.getReplacementModel(state, pos); + if (replacement != null) return replacement; + CustomBlockTextures.enterFallbackCall(); + var fallback = original.call(instance, state); + CustomBlockTextures.exitFallbackCall(); + return fallback; + } +} diff --git a/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java new file mode 100644 index 0000000..53ab74a --- /dev/null +++ b/src/main/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java @@ -0,0 +1,21 @@ +package moe.nea.firmament.mixins.custommodels; + +import moe.nea.firmament.features.texturepack.CustomBlockTextures; +import net.minecraft.block.BlockState; +import net.minecraft.client.render.block.BlockModels; +import net.minecraft.client.render.model.BakedModel; +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; + +@Mixin(BlockModels.class) +public class ReplaceFallbackBlockModel { + // TODO: add check to BlockDustParticle + @Inject(method = "getModel", at = @At("HEAD"), cancellable = true) + private void getModel(BlockState state, CallbackInfoReturnable cir) { + var replacement = CustomBlockTextures.getReplacementModel(state, null); + if (replacement != null) + cir.setReturnValue(replacement); + } +} diff --git a/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt b/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt index db073e3..f75bedc 100644 --- a/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt +++ b/src/main/kotlin/moe/nea/firmament/events/BakeExtraModelsEvent.kt @@ -5,11 +5,16 @@ import java.util.function.Consumer import net.minecraft.client.util.ModelIdentifier class BakeExtraModelsEvent( - private val addModel: Consumer, + private val addItemModel: Consumer, + private val addAnyModel: Consumer, ) : FirmamentEvent() { - fun addModel(modelIdentifier: ModelIdentifier) { - this.addModel.accept(modelIdentifier) + fun addNonItemModel(modelIdentifier: ModelIdentifier) { + this.addAnyModel.accept(modelIdentifier) + } + + fun addItemModel(modelIdentifier: ModelIdentifier) { + this.addItemModel.accept(modelIdentifier) } companion object : FirmamentEventBus() diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt new file mode 100644 index 0000000..2289be2 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt @@ -0,0 +1,277 @@ +@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class) + +package moe.nea.firmament.features.texturepack + +import java.util.concurrent.CompletableFuture +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlinx.serialization.UseSerializers +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.serializer +import kotlin.jvm.optionals.getOrNull +import net.minecraft.block.Block +import net.minecraft.block.BlockState +import net.minecraft.client.render.model.BakedModel +import net.minecraft.client.util.ModelIdentifier +import net.minecraft.registry.RegistryKey +import net.minecraft.registry.RegistryKeys +import net.minecraft.resource.ResourceManager +import net.minecraft.resource.SinglePreparationResourceReloader +import net.minecraft.util.Identifier +import net.minecraft.util.math.BlockPos +import net.minecraft.util.profiler.Profiler +import moe.nea.firmament.Firmament +import moe.nea.firmament.annotations.Subscribe +import moe.nea.firmament.events.BakeExtraModelsEvent +import moe.nea.firmament.events.EarlyResourceReloadEvent +import moe.nea.firmament.events.FinalizeResourceManagerEvent +import moe.nea.firmament.events.SkyblockServerUpdateEvent +import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger +import moe.nea.firmament.util.IdentifierSerializer +import moe.nea.firmament.util.MC +import moe.nea.firmament.util.SBData +import moe.nea.firmament.util.SkyBlockIsland +import moe.nea.firmament.util.json.BlockPosSerializer +import moe.nea.firmament.util.json.SingletonSerializableList + + +object CustomBlockTextures { + @Serializable + data class CustomBlockOverride( + val modes: @Serializable(SingletonSerializableList::class) List, + val area: List? = null, + val replacements: Map, + ) + + @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 + + object Serializer : KSerializer { + val delegate = serializer() + override val descriptor: SerialDescriptor + get() = delegate.descriptor + + override fun deserialize(decoder: Decoder): Replacement { + val jsonElement = decoder.decodeSerializableValue(delegate) + if (jsonElement is JsonPrimitive) { + require(jsonElement.isString) + return Replacement(Identifier.tryParse(jsonElement.content)!!, null) + } + return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement) + } + + override fun serialize(encoder: Encoder, value: Replacement) { + encoder.encodeSerializableValue(DefaultSerializer, value) + } + } + } + + @Serializable + data class Area( + val min: BlockPos, + val max: BlockPos, + ) { + @Transient + val realMin = BlockPos( + minOf(min.x, max.x), + minOf(min.y, max.y), + minOf(min.z, max.z), + ) + + @Transient + val realMax = BlockPos( + maxOf(min.x, max.x), + maxOf(min.y, max.y), + maxOf(min.z, max.z), + ) + + fun roughJoin(other: Area): Area { + return Area( + BlockPos( + minOf(realMin.x, other.realMin.x), + minOf(realMin.y, other.realMin.y), + minOf(realMin.z, other.realMin.z), + ), + BlockPos( + maxOf(realMax.x, other.realMax.x), + maxOf(realMax.y, other.realMax.y), + maxOf(realMax.z, other.realMax.z), + ) + ) + } + + fun contains(blockPos: BlockPos): Boolean { + return (blockPos.x in realMin.x..realMax.x) && + (blockPos.y in realMin.y..realMax.y) && + (blockPos.z in realMin.z..realMax.z) + } + } + + data class LocationReplacements( + val lookup: Map> + ) + + data class BlockReplacement( + val checks: List?, + 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) + + 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.reload() + } + } + } + + 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 = CompletableFuture.completedFuture(BakedReplacements( + mapOf())) + + val insideFallbackCall = ThreadLocal.withInitial { 0 } + + @JvmStatic + fun enterFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() + 1) + } + + fun isInFallback() = insideFallbackCall.get() > 0 + + @JvmStatic + fun exitFallbackCall() { + insideFallbackCall.set(insideFallbackCall.get() - 1) + } + + @Subscribe + fun onEarlyReload(event: EarlyResourceReloadEvent) { + preparationFuture = CompletableFuture + .supplyAsync( + { prepare(event.resourceManager) }, event.preparationExecutor) + } + + @Subscribe + fun bakeExtraModels(event: BakeExtraModelsEvent) { + preparationFuture.join().data.values + .flatMap { it.lookup.values } + .flatten() + .mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier } + .forEach { event.addNonItemModel(it) } + } + + private fun prepare(manager: ResourceManager): BakedReplacements { + val resources = manager.findResources("overrides/blocks") { + it.namespace == "firmskyblock" && it.path.endsWith(".json") + } + val map = mutableMapOf>>() + for ((file, resource) in resources) { + val json = + Firmament.tryDecodeJsonFromStream(resource.inputStream) + .getOrElse { ex -> + logger.error("Failed to load block texture override at $file", ex) + continue + } + for (mode in json.modes) { + val island = SkyBlockIsland.forMode(mode) + val islandMpa = map.getOrPut(island, ::mutableMapOf) + for ((blockId, replacement) in json.replacements) { + val block = MC.defaultRegistries.getWrapperOrThrow(RegistryKeys.BLOCK) + .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId)) + .getOrNull() + if (block == null) { + logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'") + continue + } + val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf) + replacements.add(BlockReplacement(json.area, replacement)) + } + } + } + + return BakedReplacements(map.mapValues { LocationReplacements(it.value) }) + } + + + @Subscribe + fun onStart(event: FinalizeResourceManagerEvent) { + event.resourceManager.registerReloader(object : + SinglePreparationResourceReloader() { + override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements { + return preparationFuture.join() + } + + override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) { + allLocationReplacements = prepared + refreshReplacements() + } + }) + } +} diff --git a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt index ed5981a..d64c844 100644 --- a/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt +++ b/src/main/kotlin/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt @@ -78,7 +78,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader { val delegateSerializer = String.serializer() override val descriptor: SerialDescriptor - get() = SerialDescriptor("Identifier", delegateSerializer.descriptor) + get() = PrimitiveSerialDescriptor("Identifier", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): Identifier { return Identifier.of(decoder.decodeSerializableValue(delegateSerializer)) diff --git a/src/main/kotlin/moe/nea/firmament/util/MC.kt b/src/main/kotlin/moe/nea/firmament/util/MC.kt index 8935766..b0d3056 100644 --- a/src/main/kotlin/moe/nea/firmament/util/MC.kt +++ b/src/main/kotlin/moe/nea/firmament/util/MC.kt @@ -1,11 +1,10 @@ - - package moe.nea.firmament.util import io.github.moulberry.repo.data.Coordinate import java.util.concurrent.ConcurrentLinkedQueue import net.minecraft.client.MinecraftClient import net.minecraft.client.gui.screen.ingame.HandledScreen +import net.minecraft.client.render.WorldRenderer import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket import net.minecraft.registry.BuiltinRegistries import net.minecraft.registry.RegistryKeys @@ -24,6 +23,9 @@ object MC { while (true) { inGameHud.chatHud.addMessage(messageQueue.poll() ?: break) } + while (true) { + (nextTickTodos.poll() ?: break).invoke() + } } } @@ -58,7 +60,14 @@ object MC { instance.send(block) } + private val nextTickTodos = ConcurrentLinkedQueue<() -> Unit>() + fun nextTick(function: () -> Unit) { + nextTickTodos.add(function) + } + + inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl) + inline val worldRenderer: WorldRenderer get() = instance.worldRenderer inline val networkHandler get() = player?.networkHandler inline val instance get() = MinecraftClient.getInstance() inline val keyboard get() = instance.keyboard diff --git a/src/main/kotlin/moe/nea/firmament/util/json/BlockPosSerializer.kt b/src/main/kotlin/moe/nea/firmament/util/json/BlockPosSerializer.kt new file mode 100644 index 0000000..144b0a0 --- /dev/null +++ b/src/main/kotlin/moe/nea/firmament/util/json/BlockPosSerializer.kt @@ -0,0 +1,25 @@ +package moe.nea.firmament.util.json + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.serializer +import net.minecraft.util.math.BlockPos + +object BlockPosSerializer : KSerializer { + val delegate = serializer>() + + override val descriptor: SerialDescriptor + get() = SerialDescriptor("BlockPos", delegate.descriptor) + + override fun deserialize(decoder: Decoder): BlockPos { + val list = decoder.decodeSerializableValue(delegate) + require(list.size == 3) + return BlockPos(list[0], list[1], list[2]) + } + + override fun serialize(encoder: Encoder, value: BlockPos) { + encoder.encodeSerializableValue(delegate, listOf(value.x, value.y, value.z)) + } +} diff --git a/src/main/resources/assets/firmament/lang/en_us.json b/src/main/resources/assets/firmament/lang/en_us.json index 05b10d5..dd514fe 100644 --- a/src/main/resources/assets/firmament/lang/en_us.json +++ b/src/main/resources/assets/firmament/lang/en_us.json @@ -164,6 +164,7 @@ "firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration", "firmament.config.custom-skyblock-textures.model-overrides": "Enable model overrides/predicates", "firmament.config.custom-skyblock-textures.armor-overrides": "Enable Armor re-texturing", + "firmament.config.custom-skyblock-textures.block-overrides": "Enable Block re-modelling", "firmament.config.custom-skyblock-textures.enabled": "Enable Custom Item Textures", "firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures", "firmament.config.fixes": "Fixes", -- cgit