package net.minecraft.client.resources.model; import com.google.common.base.Splitter; import com.google.common.collect.Sets; import com.google.gson.JsonElement; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.util.Collection; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Map.Entry; import java.util.function.BiConsumer; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.Util; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.renderer.block.BlockModelShaper; import net.minecraft.client.renderer.block.model.BlockModelDefinition; import net.minecraft.client.renderer.block.model.multipart.MultiPart; import net.minecraft.client.renderer.block.model.multipart.Selector; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.RenderShape; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.BooleanProperty; import net.minecraft.world.level.block.state.properties.Property; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class BlockStateModelLoader { private static final Logger LOGGER = LogUtils.getLogger(); static final int SINGLETON_MODEL_GROUP = -1; private static final int INVISIBLE_MODEL_GROUP = 0; public static final FileToIdConverter BLOCKSTATE_LISTER = FileToIdConverter.json("blockstates"); private static final Splitter COMMA_SPLITTER = Splitter.on(','); private static final Splitter EQUAL_SPLITTER = Splitter.on('=').limit(2); private static final StateDefinition ITEM_FRAME_FAKE_DEFINITION = new StateDefinition.Builder(Blocks.AIR) .add(BooleanProperty.create("map")) .create(Block::defaultBlockState, BlockState::new); private static final Map> STATIC_DEFINITIONS = Map.of( ResourceLocation.withDefaultNamespace("item_frame"), ITEM_FRAME_FAKE_DEFINITION, ResourceLocation.withDefaultNamespace("glow_item_frame"), ITEM_FRAME_FAKE_DEFINITION ); private final Map> blockStateResources; private final ProfilerFiller profiler; private final BlockColors blockColors; private final BiConsumer discoveredModelOutput; private int nextModelGroup = 1; private final Object2IntMap modelGroups = Util.make( new Object2IntOpenHashMap<>(), object2IntOpenHashMap -> object2IntOpenHashMap.defaultReturnValue(-1) ); private final BlockStateModelLoader.LoadedModel missingModel; private final BlockModelDefinition.Context context = new BlockModelDefinition.Context(); public BlockStateModelLoader( Map> blockStateResources, ProfilerFiller profiler, UnbakedModel missingModel, BlockColors blockColors, BiConsumer discoveredModelOutput ) { this.blockStateResources = blockStateResources; this.profiler = profiler; this.blockColors = blockColors; this.discoveredModelOutput = discoveredModelOutput; BlockStateModelLoader.ModelGroupKey modelGroupKey = new BlockStateModelLoader.ModelGroupKey(List.of(missingModel), List.of()); this.missingModel = new BlockStateModelLoader.LoadedModel(missingModel, () -> modelGroupKey); } public void loadAllBlockStates() { this.profiler.push("static_definitions"); STATIC_DEFINITIONS.forEach(this::loadBlockStateDefinitions); this.profiler.popPush("blocks"); for (Block block : BuiltInRegistries.BLOCK) { this.loadBlockStateDefinitions(block.builtInRegistryHolder().key().location(), block.getStateDefinition()); } this.profiler.pop(); } private void loadBlockStateDefinitions(ResourceLocation blockStateId, StateDefinition stateDefenition) { this.context.setDefinition(stateDefenition); List> list = List.copyOf(this.blockColors.getColoringProperties(stateDefenition.getOwner())); List list2 = stateDefenition.getPossibleStates(); Map map = new HashMap(); list2.forEach(blockState -> map.put(BlockModelShaper.stateToModelLocation(blockStateId, blockState), blockState)); Map map2 = new HashMap(); ResourceLocation resourceLocation = BLOCKSTATE_LISTER.idToFile(blockStateId); try { for (BlockStateModelLoader.LoadedJson loadedJson : (List)this.blockStateResources.getOrDefault(resourceLocation, List.of())) { BlockModelDefinition blockModelDefinition = loadedJson.parse(blockStateId, this.context); Map map3 = new IdentityHashMap(); MultiPart multiPart; if (blockModelDefinition.isMultiPart()) { multiPart = blockModelDefinition.getMultiPart(); list2.forEach( blockState -> map3.put( blockState, new BlockStateModelLoader.LoadedModel(multiPart, () -> BlockStateModelLoader.ModelGroupKey.create(blockState, multiPart, list)) ) ); } else { multiPart = null; } blockModelDefinition.getVariants() .forEach( (string, multiVariant) -> { try { list2.stream() .filter(predicate(stateDefenition, string)) .forEach( blockState -> { BlockStateModelLoader.LoadedModel loadedModel = (BlockStateModelLoader.LoadedModel)map3.put( blockState, new BlockStateModelLoader.LoadedModel(multiVariant, () -> BlockStateModelLoader.ModelGroupKey.create(blockState, multiVariant, list)) ); if (loadedModel != null && loadedModel.model != multiPart) { map3.put(blockState, this.missingModel); throw new RuntimeException( "Overlapping definition with: " + (String)((Entry)blockModelDefinition.getVariants().entrySet().stream().filter(entry -> entry.getValue() == loadedModel.model).findFirst().get()) .getKey() ); } } ); } catch (Exception var12x) { LOGGER.warn( "Exception loading blockstate definition: '{}' in resourcepack: '{}' for variant: '{}': {}", resourceLocation, loadedJson.source, string, var12x.getMessage() ); } } ); map2.putAll(map3); } } catch (BlockStateModelLoader.BlockStateDefinitionException var18) { LOGGER.warn("{}", var18.getMessage()); } catch (Exception var19) { LOGGER.warn("Exception loading blockstate definition: '{}'", resourceLocation, var19); } finally { Map> map5 = new HashMap(); map.forEach((modelResourceLocation, blockState) -> { BlockStateModelLoader.LoadedModel loadedModel = (BlockStateModelLoader.LoadedModel)map2.get(blockState); if (loadedModel == null) { LOGGER.warn("Exception loading blockstate definition: '{}' missing model for variant: '{}'", resourceLocation, modelResourceLocation); loadedModel = this.missingModel; } this.discoveredModelOutput.accept(modelResourceLocation, loadedModel.model); try { BlockStateModelLoader.ModelGroupKey modelGroupKey = (BlockStateModelLoader.ModelGroupKey)loadedModel.key().get(); ((Set)map5.computeIfAbsent(modelGroupKey, modelGroupKeyx -> Sets.newIdentityHashSet())).add(blockState); } catch (Exception var8) { LOGGER.warn("Exception evaluating model definition: '{}'", modelResourceLocation, var8); } }); map5.forEach((modelGroupKey, set) -> { Iterator iterator = set.iterator(); while (iterator.hasNext()) { BlockState blockState = (BlockState)iterator.next(); if (blockState.getRenderShape() != RenderShape.MODEL) { iterator.remove(); this.modelGroups.put(blockState, 0); } } if (set.size() > 1) { this.registerModelGroup(set); } }); } } private static Predicate predicate(StateDefinition stateDefentition, String properties) { Map, Comparable> map = new HashMap(); for (String string : COMMA_SPLITTER.split(properties)) { Iterator iterator = EQUAL_SPLITTER.split(string).iterator(); if (iterator.hasNext()) { String string2 = (String)iterator.next(); Property property = stateDefentition.getProperty(string2); if (property != null && iterator.hasNext()) { String string3 = (String)iterator.next(); Comparable comparable = getValueHelper((Property>)property, string3); if (comparable == null) { throw new RuntimeException("Unknown value: '" + string3 + "' for blockstate property: '" + string2 + "' " + property.getPossibleValues()); } map.put(property, comparable); } else if (!string2.isEmpty()) { throw new RuntimeException("Unknown blockstate property: '" + string2 + "'"); } } } Block block = stateDefentition.getOwner(); return blockState -> { if (blockState != null && blockState.is(block)) { for (Entry, Comparable> entry : map.entrySet()) { if (!Objects.equals(blockState.getValue((Property)entry.getKey()), entry.getValue())) { return false; } } return true; } else { return false; } }; } @Nullable static > T getValueHelper(Property property, String propertyName) { return (T)property.getValue(propertyName).orElse(null); } private void registerModelGroup(Iterable models) { int i = this.nextModelGroup++; models.forEach(blockState -> this.modelGroups.put(blockState, i)); } public Object2IntMap getModelGroups() { return this.modelGroups; } @Environment(EnvType.CLIENT) static class BlockStateDefinitionException extends RuntimeException { public BlockStateDefinitionException(String message) { super(message); } } @Environment(EnvType.CLIENT) public record LoadedJson(String source, JsonElement data) { BlockModelDefinition parse(ResourceLocation blockStateId, BlockModelDefinition.Context context) { try { return BlockModelDefinition.fromJsonElement(context, this.data); } catch (Exception var4) { throw new BlockStateModelLoader.BlockStateDefinitionException( String.format(Locale.ROOT, "Exception loading blockstate definition: '%s' in resourcepack: '%s': %s", blockStateId, this.source, var4.getMessage()) ); } } } @Environment(EnvType.CLIENT) record LoadedModel(UnbakedModel model, Supplier key) { } @Environment(EnvType.CLIENT) record ModelGroupKey(List models, List coloringValues) { public static BlockStateModelLoader.ModelGroupKey create(BlockState state, MultiPart model, Collection> properties) { StateDefinition stateDefinition = state.getBlock().getStateDefinition(); List list = (List)model.getSelectors() .stream() .filter(selector -> selector.getPredicate(stateDefinition).test(state)) .map(Selector::getVariant) .collect(Collectors.toUnmodifiableList()); List list2 = getColoringValues(state, properties); return new BlockStateModelLoader.ModelGroupKey(list, list2); } public static BlockStateModelLoader.ModelGroupKey create(BlockState state, UnbakedModel model, Collection> properties) { List list = getColoringValues(state, properties); return new BlockStateModelLoader.ModelGroupKey(List.of(model), list); } private static List getColoringValues(BlockState state, Collection> properties) { return (List)properties.stream().map(state::getValue).collect(Collectors.toUnmodifiableList()); } } }