feat: allow block states to be used for custom block models

This commit is contained in:
Linnea Gräf
2025-07-06 20:38:15 +02:00
parent a792873f7f
commit fb730fa1a6
4 changed files with 155 additions and 9 deletions

View File

@@ -16,6 +16,11 @@ accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V
accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata;
accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIIIFFFFI)V
accessible class net/minecraft/client/render/model/BlockStatesLoader$LoadedBlockStateDefinition
accessible field net/minecraft/client/render/model/BlockStatesLoader FINDER Lnet/minecraft/resource/ResourceFinder;
accessible method net/minecraft/client/render/model/BlockStatesLoader$LoadedBlockStateDefinition <init> (Ljava/lang/String;Lnet/minecraft/client/render/model/json/BlockModelDefinition;)V
accessible method net/minecraft/client/render/model/BlockStatesLoader combine (Lnet/minecraft/util/Identifier;Lnet/minecraft/state/StateManager;Ljava/util/List;)Lnet/minecraft/client/render/model/BlockStatesLoader$LoadedModels;
mutable field net/minecraft/screen/slot/Slot x I
mutable field net/minecraft/screen/slot/Slot y I

View File

@@ -2,6 +2,9 @@
package moe.nea.firmament.features.texturepack
import com.google.gson.JsonParseException
import com.google.gson.JsonParser
import com.mojang.serialization.JsonOps
import java.util.concurrent.CompletableFuture
import java.util.concurrent.Executor
import java.util.function.Function
@@ -21,15 +24,21 @@ import kotlinx.serialization.serializer
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.Block
import net.minecraft.block.BlockState
import net.minecraft.block.Blocks
import net.minecraft.client.render.model.Baker
import net.minecraft.client.render.model.BlockStateModel
import net.minecraft.client.render.model.BlockStatesLoader
import net.minecraft.client.render.model.ReferencedModelsCollector
import net.minecraft.client.render.model.SimpleBlockStateModel
import net.minecraft.client.render.model.json.BlockModelDefinition
import net.minecraft.client.render.model.json.ModelVariant
import net.minecraft.registry.Registries
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.resource.Resource
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.state.StateManager
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.profiler.Profiler
@@ -41,6 +50,7 @@ import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.features.texturepack.CustomBlockTextures.createBakedModels
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
@@ -62,12 +72,28 @@ object CustomBlockTextures {
val block: Identifier,
val sound: Identifier?,
) {
fun replace(block: BlockState): BlockStateModel? {
blockStateMap?.let { return it[block] }
return blockModel
}
@Transient
lateinit var overridingBlock: Block
@Transient
val blockModelIdentifier get() = block.withPrefixedPath("block/")
/**
* Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete.
* Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete, if [unbakedBlockStateMap] is set.
*/
@Transient
var blockStateMap: Map<BlockState, BlockStateModel>? = null
@Transient
var unbakedBlockStateMap: Map<BlockState, BlockStateModel.UnbakedGrouped>? = null
/**
* Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete. Prefer [blockStateMap] if present.
*/
@Transient
lateinit var blockModel: BlockStateModel
@@ -139,7 +165,15 @@ object CustomBlockTextures {
data class LocationReplacements(
val lookup: Map<Block, List<BlockReplacement>>
)
) {
init {
lookup.forEach { (block, replacements) ->
for (replacement in replacements) {
replacement.replacement.overridingBlock = block
}
}
}
}
data class BlockReplacement(
val checks: List<Area>?,
@@ -213,7 +247,10 @@ object CustomBlockTextures {
@JvmStatic
fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BlockStateModel? {
return getReplacement(block, blockPos)?.blockModel
if (block.block == Blocks.SMOOTH_SANDSTONE_STAIRS) {
println("WEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEWOOOOOOOOOOOOOOOOOOOOOOOOOO")
}
return getReplacement(block, blockPos)?.replace(block)
}
@JvmStatic
@@ -236,8 +273,12 @@ object CustomBlockTextures {
}
@Volatile
var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
mapOf()))
@get:JvmStatic
var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(
BakedReplacements(
mapOf()
)
)
val insideFallbackCall = ThreadLocal.withInitial { 0 }
@@ -257,7 +298,8 @@ object CustomBlockTextures {
fun onEarlyReload(event: EarlyResourceReloadEvent) {
preparationFuture = CompletableFuture
.supplyAsync(
{ prepare(event.resourceManager) }, event.preparationExecutor)
{ prepare(event.resourceManager) }, event.preparationExecutor
)
}
private fun prepare(manager: ResourceManager): BakedReplacements {
@@ -295,7 +337,7 @@ object CustomBlockTextures {
@Subscribe
fun onStart(event: FinalizeResourceManagerEvent) {
event.resourceManager.registerReloader(object :
SinglePreparationResourceReloader<BakedReplacements>() {
SinglePreparationResourceReloader<BakedReplacements>() {
override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
return preparationFuture.join().also {
it.modelBakingFuture.join()
@@ -328,12 +370,28 @@ object CustomBlockTextures {
@JvmStatic
fun collectExtraModels(modelsCollector: ReferencedModelsCollector) {
preparationFuture.join().collectAllReplacements()
.forEach { modelsCollector.resolve(simpleBlockModel(it.blockModelIdentifier)) }
.forEach {
modelsCollector.resolve(simpleBlockModel(it.blockModelIdentifier))
it.unbakedBlockStateMap?.values?.forEach {
modelsCollector.resolve(it)
}
}
}
@JvmStatic
fun createBakedModels(baker: Baker, executor: Executor): CompletableFuture<Void?> {
return preparationFuture.thenComposeAsync(Function { replacements ->
val allBlockStates = CompletableFuture.allOf(
*replacements.collectAllReplacements().filter { it.unbakedBlockStateMap != null }.map {
CompletableFuture.supplyAsync({
it.blockStateMap = it.unbakedBlockStateMap
?.map {
it.key to it.value.bake(it.key, baker)
}
?.toMap()
}, executor)
}.toList().toTypedArray()
)
val byModel = replacements.collectAllReplacements().groupBy { it.blockModelIdentifier }
val modelBakingTask = AsyncHelper.mapValues(byModel, { blockId, replacements ->
val unbakedModel = SimpleBlockStateModel.Unbaked(
@@ -344,7 +402,55 @@ object CustomBlockTextures {
it.blockModel = baked
}
}, executor)
modelBakingTask.thenAcceptAsync { replacements.modelBakingFuture.complete(Unit) }
modelBakingTask.thenComposeAsync {
allBlockStates
}.thenAcceptAsync {
replacements.modelBakingFuture.complete(Unit)
}
}, executor)
}
@JvmStatic
fun collectExtraBlockStateMaps(
extra: BakedReplacements,
original: Map<Identifier, List<Resource>>,
stateManagers: Function<Identifier, StateManager<Block, BlockState>?>
) {
extra.collectAllReplacements().forEach {
val blockId = Registries.BLOCK.getKey(it.overridingBlock).getOrNull()?.value ?: return@forEach
val allModels = mutableListOf<BlockStatesLoader.LoadedBlockStateDefinition>()
val stateManager = stateManagers.apply(blockId) ?: return@forEach
for (resource in original[BlockStatesLoader.FINDER.toResourcePath(it.block)] ?: return@forEach) {
try {
resource.reader.use { reader ->
val jsonElement = JsonParser.parseReader(reader)
val blockModelDefinition =
BlockModelDefinition.CODEC.parse(JsonOps.INSTANCE, jsonElement)
.getOrThrow { msg: String? -> JsonParseException(msg) }
allModels.add(
BlockStatesLoader.LoadedBlockStateDefinition(
resource.getPackId(),
blockModelDefinition
)
)
}
} catch (exception: Exception) {
ErrorUtil.softError(
"Failed to load custom blockstate definition ${it.block} from pack ${resource.packId}",
exception
)
}
}
try {
it.unbakedBlockStateMap = BlockStatesLoader.combine(
blockId,
stateManager,
allModels
).models
} catch (exception: Exception) {
ErrorUtil.softError("Failed to combine custom blockstate definitions for ${it.block}", exception)
}
}
}
}

View File

@@ -1,4 +1,34 @@
package moe.nea.firmament.mixins.custommodels;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.features.texturepack.CustomBlockTextures;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.model.BlockStatesLoader;
import net.minecraft.resource.Resource;
import net.minecraft.state.StateManager;
import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
@Mixin(BlockStatesLoader.class)
public class LoadExtraBlockStates {
@ModifyExpressionValue(method = "load", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;supplyAsync(Ljava/util/function/Supplier;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private static CompletableFuture<Map<Identifier, List<Resource>>> loadExtraModels(
CompletableFuture<Map<Identifier, List<Resource>>> x,
@Local(argsOnly = true) Executor executor,
@Local Function<Identifier, StateManager<Block, BlockState>> stateManagers
) {
return x.thenCombineAsync(CustomBlockTextures.getPreparationFuture(), (original, extra) -> {
CustomBlockTextures.collectExtraBlockStateMaps(extra, original, stateManagers);
return original;
}, executor);
}
}