Add custom block textures
This commit is contained in:
@@ -139,6 +139,7 @@ dependencies {
|
|||||||
modRuntimeOnly(libs.fabric.api.deprecated)
|
modRuntimeOnly(libs.fabric.api.deprecated)
|
||||||
modApi(libs.architectury)
|
modApi(libs.architectury)
|
||||||
modCompileOnly(libs.jarvis.api)
|
modCompileOnly(libs.jarvis.api)
|
||||||
|
modCompileOnly(libs.sodium)
|
||||||
include(libs.jarvis.fabric)
|
include(libs.jarvis.fabric)
|
||||||
|
|
||||||
modCompileOnly(libs.femalegender)
|
modCompileOnly(libs.femalegender)
|
||||||
|
|||||||
@@ -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
|
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).
|
`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/<id>.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/<namespace>/models/block/<path>.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.
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
package moe.nea.firmament.mixins;
|
package moe.nea.firmament.mixins;
|
||||||
|
|
||||||
import moe.nea.firmament.events.BakeExtraModelsEvent;
|
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.Final;
|
||||||
import org.spongepowered.asm.mixin.Mixin;
|
import org.spongepowered.asm.mixin.Mixin;
|
||||||
import org.spongepowered.asm.mixin.Shadow;
|
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.At;
|
||||||
import org.spongepowered.asm.mixin.injection.Inject;
|
import org.spongepowered.asm.mixin.injection.Inject;
|
||||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
@@ -29,9 +28,19 @@ public abstract class CustomModelBakerPatch {
|
|||||||
@Shadow
|
@Shadow
|
||||||
abstract UnbakedModel getOrLoadModel(Identifier id);
|
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"))
|
@Inject(method = "bake", at = @At("HEAD"))
|
||||||
public void onBake(ModelLoader.SpriteGetter spliteGetter, CallbackInfo ci) {
|
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.values().forEach(model -> model.setParents(this::getOrLoadModel));
|
||||||
// modelsToBake.keySet().stream()
|
// modelsToBake.keySet().stream()
|
||||||
// .filter(it -> !it.id().getNamespace().equals("minecraft"))
|
// .filter(it -> !it.id().getNamespace().equals("minecraft"))
|
||||||
|
|||||||
@@ -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<BakedModel> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SoundEvent> 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);
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -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<PositionedSoundInstance> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BakedModel> 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<BakedModel> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BakedModel> cir) {
|
||||||
|
var replacement = CustomBlockTextures.getReplacementModel(state, null);
|
||||||
|
if (replacement != null)
|
||||||
|
cir.setReturnValue(replacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,11 +5,16 @@ import java.util.function.Consumer
|
|||||||
import net.minecraft.client.util.ModelIdentifier
|
import net.minecraft.client.util.ModelIdentifier
|
||||||
|
|
||||||
class BakeExtraModelsEvent(
|
class BakeExtraModelsEvent(
|
||||||
private val addModel: Consumer<ModelIdentifier>,
|
private val addItemModel: Consumer<ModelIdentifier>,
|
||||||
|
private val addAnyModel: Consumer<ModelIdentifier>,
|
||||||
) : FirmamentEvent() {
|
) : FirmamentEvent() {
|
||||||
|
|
||||||
fun addModel(modelIdentifier: ModelIdentifier) {
|
fun addNonItemModel(modelIdentifier: ModelIdentifier) {
|
||||||
this.addModel.accept(modelIdentifier)
|
this.addAnyModel.accept(modelIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addItemModel(modelIdentifier: ModelIdentifier) {
|
||||||
|
this.addItemModel.accept(modelIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object : FirmamentEventBus<BakeExtraModelsEvent>()
|
companion object : FirmamentEventBus<BakeExtraModelsEvent>()
|
||||||
|
|||||||
@@ -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<String>,
|
||||||
|
val area: List<Area>? = null,
|
||||||
|
val replacements: Map<Identifier, Replacement>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable(with = Replacement.Serializer::class)
|
||||||
|
data class Replacement(
|
||||||
|
val block: Identifier,
|
||||||
|
val sound: Identifier?,
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament")
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
MC.instance.bakedModelManager.getModel(blockModelIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@kotlinx.serialization.Serializer(Replacement::class)
|
||||||
|
object DefaultSerializer : KSerializer<Replacement>
|
||||||
|
|
||||||
|
object Serializer : KSerializer<Replacement> {
|
||||||
|
val delegate = serializer<JsonElement>()
|
||||||
|
override val descriptor: SerialDescriptor
|
||||||
|
get() = delegate.descriptor
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Replacement {
|
||||||
|
val jsonElement = decoder.decodeSerializableValue(delegate)
|
||||||
|
if (jsonElement is JsonPrimitive) {
|
||||||
|
require(jsonElement.isString)
|
||||||
|
return Replacement(Identifier.tryParse(jsonElement.content)!!, null)
|
||||||
|
}
|
||||||
|
return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Replacement) {
|
||||||
|
encoder.encodeSerializableValue(DefaultSerializer, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Area(
|
||||||
|
val min: BlockPos,
|
||||||
|
val max: BlockPos,
|
||||||
|
) {
|
||||||
|
@Transient
|
||||||
|
val realMin = BlockPos(
|
||||||
|
minOf(min.x, max.x),
|
||||||
|
minOf(min.y, max.y),
|
||||||
|
minOf(min.z, max.z),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
val realMax = BlockPos(
|
||||||
|
maxOf(min.x, max.x),
|
||||||
|
maxOf(min.y, max.y),
|
||||||
|
maxOf(min.z, max.z),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun roughJoin(other: Area): Area {
|
||||||
|
return Area(
|
||||||
|
BlockPos(
|
||||||
|
minOf(realMin.x, other.realMin.x),
|
||||||
|
minOf(realMin.y, other.realMin.y),
|
||||||
|
minOf(realMin.z, other.realMin.z),
|
||||||
|
),
|
||||||
|
BlockPos(
|
||||||
|
maxOf(realMax.x, other.realMax.x),
|
||||||
|
maxOf(realMax.y, other.realMax.y),
|
||||||
|
maxOf(realMax.z, other.realMax.z),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contains(blockPos: BlockPos): Boolean {
|
||||||
|
return (blockPos.x in realMin.x..realMax.x) &&
|
||||||
|
(blockPos.y in realMin.y..realMax.y) &&
|
||||||
|
(blockPos.z in realMin.z..realMax.z)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LocationReplacements(
|
||||||
|
val lookup: Map<Block, List<BlockReplacement>>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BlockReplacement(
|
||||||
|
val checks: List<Area>?,
|
||||||
|
val replacement: Replacement,
|
||||||
|
) {
|
||||||
|
val roughCheck by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
if (checks == null || checks.size < 3) return@lazy null
|
||||||
|
checks.reduce { acc, next -> acc.roughJoin(next) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>)
|
||||||
|
|
||||||
|
var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf())
|
||||||
|
var currentIslandReplacements: LocationReplacements? = null
|
||||||
|
|
||||||
|
fun refreshReplacements() {
|
||||||
|
val location = SBData.skyblockLocation
|
||||||
|
val replacements =
|
||||||
|
if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get)
|
||||||
|
else null
|
||||||
|
val lastReplacements = currentIslandReplacements
|
||||||
|
currentIslandReplacements = replacements
|
||||||
|
if (lastReplacements != replacements) {
|
||||||
|
MC.nextTick {
|
||||||
|
MC.worldRenderer.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<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
|
||||||
|
mapOf()))
|
||||||
|
|
||||||
|
val insideFallbackCall = ThreadLocal.withInitial { 0 }
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun enterFallbackCall() {
|
||||||
|
insideFallbackCall.set(insideFallbackCall.get() + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isInFallback() = insideFallbackCall.get() > 0
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun exitFallbackCall() {
|
||||||
|
insideFallbackCall.set(insideFallbackCall.get() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun onEarlyReload(event: EarlyResourceReloadEvent) {
|
||||||
|
preparationFuture = CompletableFuture
|
||||||
|
.supplyAsync(
|
||||||
|
{ prepare(event.resourceManager) }, event.preparationExecutor)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
fun bakeExtraModels(event: BakeExtraModelsEvent) {
|
||||||
|
preparationFuture.join().data.values
|
||||||
|
.flatMap { it.lookup.values }
|
||||||
|
.flatten()
|
||||||
|
.mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier }
|
||||||
|
.forEach { event.addNonItemModel(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepare(manager: ResourceManager): BakedReplacements {
|
||||||
|
val resources = manager.findResources("overrides/blocks") {
|
||||||
|
it.namespace == "firmskyblock" && it.path.endsWith(".json")
|
||||||
|
}
|
||||||
|
val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>()
|
||||||
|
for ((file, resource) in resources) {
|
||||||
|
val json =
|
||||||
|
Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream)
|
||||||
|
.getOrElse { ex ->
|
||||||
|
logger.error("Failed to load block texture override at $file", ex)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (mode in json.modes) {
|
||||||
|
val island = SkyBlockIsland.forMode(mode)
|
||||||
|
val islandMpa = map.getOrPut(island, ::mutableMapOf)
|
||||||
|
for ((blockId, replacement) in json.replacements) {
|
||||||
|
val block = MC.defaultRegistries.getWrapperOrThrow(RegistryKeys.BLOCK)
|
||||||
|
.getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId))
|
||||||
|
.getOrNull()
|
||||||
|
if (block == null) {
|
||||||
|
logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf)
|
||||||
|
replacements.add(BlockReplacement(json.area, replacement))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return BakedReplacements(map.mapValues { LocationReplacements(it.value) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,7 +78,7 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText
|
|||||||
fun onBakeModels(event: BakeExtraModelsEvent) {
|
fun onBakeModels(event: BakeExtraModelsEvent) {
|
||||||
for (guiClassOverride in preparationFuture.join().classes) {
|
for (guiClassOverride in preparationFuture.join().classes) {
|
||||||
for (override in guiClassOverride.overrides) {
|
for (override in guiClassOverride.overrides) {
|
||||||
event.addModel(ModelIdentifier(override.model, "inventory"))
|
event.addItemModel(ModelIdentifier(override.model, "inventory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
|
|
||||||
|
|
||||||
package moe.nea.firmament.features.texturepack
|
package moe.nea.firmament.features.texturepack
|
||||||
|
|
||||||
import com.mojang.authlib.minecraft.MinecraftProfileTexture
|
import com.mojang.authlib.minecraft.MinecraftProfileTexture
|
||||||
@@ -31,6 +29,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
|
|||||||
val cacheDuration by integer("cache-duration", 0, 20) { 1 }
|
val cacheDuration by integer("cache-duration", 0, 20) { 1 }
|
||||||
val enableModelOverrides by toggle("model-overrides") { true }
|
val enableModelOverrides by toggle("model-overrides") { true }
|
||||||
val enableArmorOverrides by toggle("armor-overrides") { true }
|
val enableArmorOverrides by toggle("armor-overrides") { true }
|
||||||
|
val enableBlockOverrides by toggle("block-overrides") { true }
|
||||||
}
|
}
|
||||||
|
|
||||||
override val config: ManagedConfig
|
override val config: ManagedConfig
|
||||||
@@ -63,7 +62,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
|
|||||||
"models/item/".length,
|
"models/item/".length,
|
||||||
identifier.path.length - ".json".length),
|
identifier.path.length - ".json".length),
|
||||||
))
|
))
|
||||||
event.addModel(modelId)
|
event.addItemModel(modelId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package moe.nea.firmament.util
|
|||||||
|
|
||||||
import kotlinx.serialization.KSerializer
|
import kotlinx.serialization.KSerializer
|
||||||
import kotlinx.serialization.builtins.serializer
|
import kotlinx.serialization.builtins.serializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
import kotlinx.serialization.encoding.Decoder
|
import kotlinx.serialization.encoding.Decoder
|
||||||
import kotlinx.serialization.encoding.Encoder
|
import kotlinx.serialization.encoding.Encoder
|
||||||
@@ -11,7 +13,7 @@ import net.minecraft.util.Identifier
|
|||||||
object IdentifierSerializer : KSerializer<Identifier> {
|
object IdentifierSerializer : KSerializer<Identifier> {
|
||||||
val delegateSerializer = String.serializer()
|
val delegateSerializer = String.serializer()
|
||||||
override val descriptor: SerialDescriptor
|
override val descriptor: SerialDescriptor
|
||||||
get() = SerialDescriptor("Identifier", delegateSerializer.descriptor)
|
get() = PrimitiveSerialDescriptor("Identifier", PrimitiveKind.STRING)
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): Identifier {
|
override fun deserialize(decoder: Decoder): Identifier {
|
||||||
return Identifier.of(decoder.decodeSerializableValue(delegateSerializer))
|
return Identifier.of(decoder.decodeSerializableValue(delegateSerializer))
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
package moe.nea.firmament.util
|
package moe.nea.firmament.util
|
||||||
|
|
||||||
import io.github.moulberry.repo.data.Coordinate
|
import io.github.moulberry.repo.data.Coordinate
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue
|
import java.util.concurrent.ConcurrentLinkedQueue
|
||||||
import net.minecraft.client.MinecraftClient
|
import net.minecraft.client.MinecraftClient
|
||||||
import net.minecraft.client.gui.screen.ingame.HandledScreen
|
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.network.packet.c2s.play.CommandExecutionC2SPacket
|
||||||
import net.minecraft.registry.BuiltinRegistries
|
import net.minecraft.registry.BuiltinRegistries
|
||||||
import net.minecraft.registry.RegistryKeys
|
import net.minecraft.registry.RegistryKeys
|
||||||
@@ -24,6 +23,9 @@ object MC {
|
|||||||
while (true) {
|
while (true) {
|
||||||
inGameHud.chatHud.addMessage(messageQueue.poll() ?: break)
|
inGameHud.chatHud.addMessage(messageQueue.poll() ?: break)
|
||||||
}
|
}
|
||||||
|
while (true) {
|
||||||
|
(nextTickTodos.poll() ?: break).invoke()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +60,14 @@ object MC {
|
|||||||
instance.send(block)
|
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 resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl)
|
||||||
|
inline val worldRenderer: WorldRenderer get() = instance.worldRenderer
|
||||||
inline val networkHandler get() = player?.networkHandler
|
inline val networkHandler get() = player?.networkHandler
|
||||||
inline val instance get() = MinecraftClient.getInstance()
|
inline val instance get() = MinecraftClient.getInstance()
|
||||||
inline val keyboard get() = instance.keyboard
|
inline val keyboard get() = instance.keyboard
|
||||||
|
|||||||
@@ -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<BlockPos> {
|
||||||
|
val delegate = serializer<List<Int>>()
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -164,6 +164,7 @@
|
|||||||
"firmament.config.custom-skyblock-textures.cache-duration": "Model Cache Duration",
|
"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.model-overrides": "Enable model overrides/predicates",
|
||||||
"firmament.config.custom-skyblock-textures.armor-overrides": "Enable Armor re-texturing",
|
"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.enabled": "Enable Custom Item Textures",
|
||||||
"firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures",
|
"firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures",
|
||||||
"firmament.config.fixes": "Fixes",
|
"firmament.config.fixes": "Fixes",
|
||||||
|
|||||||
Reference in New Issue
Block a user