package net.minecraft.client.resources.model; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.mojang.datafixers.util.Pair; import com.mojang.logging.LogUtils; import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMaps; import java.io.Reader; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.Map.Entry; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import net.minecraft.Util; import net.minecraft.client.color.block.BlockColors; import net.minecraft.client.model.geom.EntityModelSet; import net.minecraft.client.renderer.Sheets; import net.minecraft.client.renderer.SpecialBlockModelRenderer; import net.minecraft.client.renderer.block.BlockModelShaper; import net.minecraft.client.renderer.block.model.BlockModel; import net.minecraft.client.renderer.item.ClientItem; import net.minecraft.client.renderer.item.ItemModel; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.resources.FileToIdConverter; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.packs.resources.PreparableReloadListener; import net.minecraft.server.packs.resources.Resource; import net.minecraft.server.packs.resources.ResourceManager; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; import org.slf4j.Logger; @OnlyIn(Dist.CLIENT) public class ModelManager implements PreparableReloadListener, AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private static final FileToIdConverter MODEL_LISTER = FileToIdConverter.json("models"); private static final Map VANILLA_ATLASES = Map.of( Sheets.BANNER_SHEET, ResourceLocation.withDefaultNamespace("banner_patterns"), Sheets.BED_SHEET, ResourceLocation.withDefaultNamespace("beds"), Sheets.CHEST_SHEET, ResourceLocation.withDefaultNamespace("chests"), Sheets.SHIELD_SHEET, ResourceLocation.withDefaultNamespace("shield_patterns"), Sheets.SIGN_SHEET, ResourceLocation.withDefaultNamespace("signs"), Sheets.SHULKER_SHEET, ResourceLocation.withDefaultNamespace("shulker_boxes"), Sheets.ARMOR_TRIMS_SHEET, ResourceLocation.withDefaultNamespace("armor_trims"), Sheets.DECORATED_POT_SHEET, ResourceLocation.withDefaultNamespace("decorated_pot"), TextureAtlas.LOCATION_BLOCKS, ResourceLocation.withDefaultNamespace("blocks") ); private Map bakedBlockStateModels = Map.of(); private Map bakedItemStackModels = Map.of(); private Map itemProperties = Map.of(); private final AtlasSet atlases; private final BlockModelShaper blockModelShaper; private final BlockColors blockColors; private EntityModelSet entityModelSet = EntityModelSet.EMPTY; private SpecialBlockModelRenderer specialBlockModelRenderer = SpecialBlockModelRenderer.EMPTY; private int maxMipmapLevels; private BakedModel missingModel; private ItemModel missingItemModel; private Object2IntMap modelGroups = Object2IntMaps.emptyMap(); public ModelManager(TextureManager p_119406_, BlockColors p_119407_, int p_119408_) { this.blockColors = p_119407_; this.maxMipmapLevels = p_119408_; this.blockModelShaper = new BlockModelShaper(this); this.atlases = new AtlasSet(VANILLA_ATLASES, p_119406_); } public BakedModel getModel(ModelResourceLocation p_119423_) { return this.bakedBlockStateModels.getOrDefault(p_119423_, this.missingModel); } public BakedModel getMissingModel() { return this.missingModel; } public ItemModel getItemModel(ResourceLocation p_376816_) { return this.bakedItemStackModels.getOrDefault(p_376816_, this.missingItemModel); } public ClientItem.Properties getItemProperties(ResourceLocation p_378319_) { return this.itemProperties.getOrDefault(p_378319_, ClientItem.Properties.DEFAULT); } public BlockModelShaper getBlockModelShaper() { return this.blockModelShaper; } @Override public final CompletableFuture reload( PreparableReloadListener.PreparationBarrier p_249079_, ResourceManager p_251134_, Executor p_250550_, Executor p_249221_ ) { UnbakedModel unbakedmodel = MissingBlockModel.missingModel(); CompletableFuture completablefuture = CompletableFuture.supplyAsync(EntityModelSet::vanilla, p_250550_); CompletableFuture completablefuture1 = completablefuture.thenApplyAsync(SpecialBlockModelRenderer::vanilla, p_250550_); CompletableFuture> completablefuture2 = loadBlockModels(p_251134_, p_250550_); CompletableFuture completablefuture3 = BlockStateModelLoader.loadBlockStates(unbakedmodel, p_251134_, p_250550_); CompletableFuture completablefuture4 = ClientItemInfoLoader.scheduleLoad(p_251134_, p_250550_); CompletableFuture completablefuture5 = CompletableFuture.allOf(completablefuture2, completablefuture3, completablefuture4) .thenApplyAsync(p_374722_ -> discoverModelDependencies(unbakedmodel, completablefuture2.join(), completablefuture3.join(), completablefuture4.join()), p_250550_); CompletableFuture> completablefuture6 = completablefuture3.thenApplyAsync( p_358038_ -> buildModelGroups(this.blockColors, p_358038_), p_250550_ ); Map> map = this.atlases.scheduleLoad(p_251134_, this.maxMipmapLevels, p_250550_); return CompletableFuture.allOf( Stream.concat( map.values().stream(), Stream.of(completablefuture5, completablefuture6, completablefuture3, completablefuture4, completablefuture, completablefuture1) ) .toArray(CompletableFuture[]::new) ) .thenApplyAsync( p_374732_ -> { Map map1 = map.entrySet() .stream() .collect(Collectors.toMap(Entry::getKey, p_248988_ -> p_248988_.getValue().join())); ModelDiscovery modeldiscovery = completablefuture5.join(); Object2IntMap object2intmap = completablefuture6.join(); Set set = modeldiscovery.getUnreferencedModels(); if (!set.isEmpty()) { LOGGER.debug( "Unreferenced models: \n{}", set.stream().sorted().map(p_374723_ -> "\t" + p_374723_ + "\n").collect(Collectors.joining()) ); } ModelBakery modelbakery = new ModelBakery( completablefuture.join(), completablefuture3.join().plainModels(), completablefuture4.join().contents(), modeldiscovery.getReferencedModels(), unbakedmodel ); return loadModels(Profiler.get(), map1, modelbakery, object2intmap, completablefuture.join(), completablefuture1.join()); }, p_250550_ ) .thenCompose(p_252255_ -> p_252255_.readyForUpload.thenApply(p_251581_ -> (ModelManager.ReloadState)p_252255_)) .thenCompose(p_249079_::wait) .thenAcceptAsync(p_358039_ -> this.apply(p_358039_, Profiler.get()), p_249221_); } private static CompletableFuture> loadBlockModels(ResourceManager p_251361_, Executor p_252189_) { return CompletableFuture.>supplyAsync(() -> MODEL_LISTER.listMatchingResources(p_251361_), p_252189_) .thenCompose( p_250597_ -> { List>> list = new ArrayList<>(p_250597_.size()); for (Entry entry : p_250597_.entrySet()) { list.add(CompletableFuture.supplyAsync(() -> { ResourceLocation resourcelocation = MODEL_LISTER.fileToId(entry.getKey()); try { Pair pair; try (Reader reader = entry.getValue().openAsReader()) { pair = Pair.of(resourcelocation, BlockModel.fromStream(reader)); } return pair; } catch (Exception exception) { LOGGER.error("Failed to load model {}", entry.getKey(), exception); return null; } }, p_252189_)); } return Util.sequence(list) .thenApply( p_250813_ -> p_250813_.stream().filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Pair::getFirst, Pair::getSecond)) ); } ); } private static ModelDiscovery discoverModelDependencies( UnbakedModel p_364387_, Map p_360749_, BlockStateModelLoader.LoadedModels p_366446_, ClientItemInfoLoader.LoadedClientInfos p_378505_ ) { ModelDiscovery modeldiscovery = new ModelDiscovery(p_360749_, p_364387_); p_366446_.forResolving().forEach(modeldiscovery::addRoot); p_378505_.contents().values().forEach(p_374734_ -> modeldiscovery.addRoot(p_374734_.model())); modeldiscovery.registerSpecialModels(); modeldiscovery.discoverDependencies(); return modeldiscovery; } private static ModelManager.ReloadState loadModels( ProfilerFiller p_252136_, final Map p_250646_, ModelBakery p_248945_, Object2IntMap p_361513_, EntityModelSet p_378097_, SpecialBlockModelRenderer p_377275_ ) { p_252136_.push("baking"); final Multimap multimap = HashMultimap.create(); final Multimap multimap1 = HashMultimap.create(); final TextureAtlasSprite textureatlassprite = p_250646_.get(TextureAtlas.LOCATION_BLOCKS).missing(); ModelBakery.BakingResult modelbakery$bakingresult = p_248945_.bakeModels(new ModelBakery.TextureGetter() { @Override public TextureAtlasSprite get(ModelDebugName p_375833_, Material p_375858_) { AtlasSet.StitchResult atlasset$stitchresult = p_250646_.get(p_375858_.atlasLocation()); TextureAtlasSprite textureatlassprite1 = atlasset$stitchresult.getSprite(p_375858_.texture()); if (textureatlassprite1 != null) { return textureatlassprite1; } else { multimap.put(p_375833_.get(), p_375858_); return atlasset$stitchresult.missing(); } } @Override public TextureAtlasSprite reportMissingReference(ModelDebugName p_377684_, String p_378821_) { multimap1.put(p_377684_.get(), p_378821_); return textureatlassprite; } }); multimap.asMap() .forEach( (p_376688_, p_252017_) -> LOGGER.warn( "Missing textures in model {}:\n{}", p_376688_, p_252017_.stream() .sorted(Material.COMPARATOR) .map(p_325574_ -> " " + p_325574_.atlasLocation() + ":" + p_325574_.texture()) .collect(Collectors.joining("\n")) ) ); multimap1.asMap() .forEach( (p_374739_, p_374740_) -> LOGGER.warn( "Missing texture references in model {}:\n{}", p_374739_, p_374740_.stream().sorted().map(p_374742_ -> " " + p_374742_).collect(Collectors.joining("\n")) ) ); p_252136_.popPush("dispatch"); Map map = createBlockStateToModelDispatch(modelbakery$bakingresult.blockStateModels(), modelbakery$bakingresult.missingModel()); CompletableFuture completablefuture = CompletableFuture.allOf( p_250646_.values().stream().map(AtlasSet.StitchResult::readyForUpload).toArray(CompletableFuture[]::new) ); p_252136_.pop(); return new ModelManager.ReloadState(modelbakery$bakingresult, p_361513_, map, p_250646_, p_378097_, p_377275_, completablefuture); } private static Map createBlockStateToModelDispatch(Map p_377857_, BakedModel p_378390_) { Map map = new IdentityHashMap<>(); for (Block block : BuiltInRegistries.BLOCK) { block.getStateDefinition().getPossibleStates().forEach(p_374738_ -> { ResourceLocation resourcelocation = p_374738_.getBlock().builtInRegistryHolder().key().location(); ModelResourceLocation modelresourcelocation = BlockModelShaper.stateToModelLocation(resourcelocation, p_374738_); BakedModel bakedmodel = p_377857_.get(modelresourcelocation); if (bakedmodel == null) { LOGGER.warn("Missing model for variant: '{}'", modelresourcelocation); map.putIfAbsent(p_374738_, p_378390_); } else { map.put(p_374738_, bakedmodel); } }); } return map; } private static Object2IntMap buildModelGroups(BlockColors p_369941_, BlockStateModelLoader.LoadedModels p_360724_) { return ModelGroupCollector.build(p_369941_, p_360724_); } private void apply(ModelManager.ReloadState p_248996_, ProfilerFiller p_251960_) { p_251960_.push("upload"); p_248996_.atlasPreparations.values().forEach(AtlasSet.StitchResult::upload); ModelBakery.BakingResult modelbakery$bakingresult = p_248996_.bakedModels; this.bakedBlockStateModels = modelbakery$bakingresult.blockStateModels(); this.bakedItemStackModels = modelbakery$bakingresult.itemStackModels(); this.itemProperties = modelbakery$bakingresult.itemProperties(); this.modelGroups = p_248996_.modelGroups; this.missingModel = modelbakery$bakingresult.missingModel(); this.missingItemModel = modelbakery$bakingresult.missingItemModel(); p_251960_.popPush("cache"); this.blockModelShaper.replaceCache(p_248996_.modelCache); this.specialBlockModelRenderer = p_248996_.specialBlockModelRenderer; this.entityModelSet = p_248996_.entityModelSet; p_251960_.pop(); } public boolean requiresRender(BlockState p_119416_, BlockState p_119417_) { if (p_119416_ == p_119417_) { return false; } else { int i = this.modelGroups.getInt(p_119416_); if (i != -1) { int j = this.modelGroups.getInt(p_119417_); if (i == j) { FluidState fluidstate = p_119416_.getFluidState(); FluidState fluidstate1 = p_119417_.getFluidState(); return fluidstate != fluidstate1; } } return true; } } public TextureAtlas getAtlas(ResourceLocation p_119429_) { return this.atlases.getAtlas(p_119429_); } @Override public void close() { this.atlases.close(); } public void updateMaxMipLevel(int p_119411_) { this.maxMipmapLevels = p_119411_; } public Supplier specialBlockModelRenderer() { return () -> this.specialBlockModelRenderer; } public Supplier entityModels() { return () -> this.entityModelSet; } @OnlyIn(Dist.CLIENT) static record ReloadState( ModelBakery.BakingResult bakedModels, Object2IntMap modelGroups, Map modelCache, Map atlasPreparations, EntityModelSet entityModelSet, SpecialBlockModelRenderer specialBlockModelRenderer, CompletableFuture readyForUpload ) { } }