package net.minecraft.client.resources.model; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; 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.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.fabricmc.api.EnvType; import net.fabricmc.api.Environment; 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.block.model.BlockStateModel; import net.minecraft.client.renderer.block.model.ItemModelGenerator; 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.util.profiling.Zone; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.material.FluidState; import org.slf4j.Logger; @Environment(EnvType.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, AtlasIds.BANNER_PATTERNS, Sheets.BED_SHEET, AtlasIds.BEDS, Sheets.CHEST_SHEET, AtlasIds.CHESTS, Sheets.SHIELD_SHEET, AtlasIds.SHIELD_PATTERNS, Sheets.SIGN_SHEET, AtlasIds.SIGNS, Sheets.SHULKER_SHEET, AtlasIds.SHULKER_BOXES, Sheets.ARMOR_TRIMS_SHEET, AtlasIds.ARMOR_TRIMS, Sheets.DECORATED_POT_SHEET, AtlasIds.DECORATED_POT, TextureAtlas.LOCATION_BLOCKS, AtlasIds.BLOCKS ); 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 ModelBakery.MissingModels missingModels; private Object2IntMap modelGroups = Object2IntMaps.emptyMap(); public ModelManager(TextureManager textureManager, BlockColors blockColors, int maxMipmapLevels) { this.blockColors = blockColors; this.maxMipmapLevels = maxMipmapLevels; this.blockModelShaper = new BlockModelShaper(this); this.atlases = new AtlasSet(VANILLA_ATLASES, textureManager); } public BlockStateModel getMissingBlockStateModel() { return this.missingModels.block(); } public ItemModel getItemModel(ResourceLocation modelLocation) { return (ItemModel)this.bakedItemStackModels.getOrDefault(modelLocation, this.missingModels.item()); } public ClientItem.Properties getItemProperties(ResourceLocation itemId) { return (ClientItem.Properties)this.itemProperties.getOrDefault(itemId, ClientItem.Properties.DEFAULT); } public BlockModelShaper getBlockModelShaper() { return this.blockModelShaper; } @Override public final CompletableFuture reload( PreparableReloadListener.PreparationBarrier preparationBarrier, ResourceManager resourceManager, Executor executor, Executor executor2 ) { CompletableFuture completableFuture = CompletableFuture.supplyAsync(EntityModelSet::vanilla, executor); CompletableFuture completableFuture2 = completableFuture.thenApplyAsync(SpecialBlockModelRenderer::vanilla, executor); CompletableFuture> completableFuture3 = loadBlockModels(resourceManager, executor); CompletableFuture completableFuture4 = BlockStateModelLoader.loadBlockStates(resourceManager, executor); CompletableFuture completableFuture5 = ClientItemInfoLoader.scheduleLoad(resourceManager, executor); CompletableFuture completableFuture6 = CompletableFuture.allOf(completableFuture3, completableFuture4, completableFuture5) .thenApplyAsync( void_ -> discoverModelDependencies( (Map)completableFuture3.join(), (BlockStateModelLoader.LoadedModels)completableFuture4.join(), (ClientItemInfoLoader.LoadedClientInfos)completableFuture5.join() ), executor ); CompletableFuture> completableFuture7 = completableFuture4.thenApplyAsync( loadedModels -> buildModelGroups(this.blockColors, loadedModels), executor ); Map> map = this.atlases.scheduleLoad(resourceManager, this.maxMipmapLevels, executor); return CompletableFuture.allOf( (CompletableFuture[])Stream.concat( map.values().stream(), Stream.of(completableFuture6, completableFuture7, completableFuture4, completableFuture5, completableFuture, completableFuture2, completableFuture3) ) .toArray(CompletableFuture[]::new) ) .thenComposeAsync( void_ -> { Map map2 = Util.mapValues(map, CompletableFuture::join); ModelManager.ResolvedModels resolvedModels = (ModelManager.ResolvedModels)completableFuture6.join(); Object2IntMap object2IntMap = (Object2IntMap)completableFuture7.join(); Set set = Sets.difference(((Map)completableFuture3.join()).keySet(), resolvedModels.models.keySet()); if (!set.isEmpty()) { LOGGER.debug("Unreferenced models: \n{}", set.stream().sorted().map(resourceLocation -> "\t" + resourceLocation + "\n").collect(Collectors.joining())); } ModelBakery modelBakery = new ModelBakery( (EntityModelSet)completableFuture.join(), ((BlockStateModelLoader.LoadedModels)completableFuture4.join()).models(), ((ClientItemInfoLoader.LoadedClientInfos)completableFuture5.join()).contents(), resolvedModels.models(), resolvedModels.missing() ); return loadModels( map2, modelBakery, object2IntMap, (EntityModelSet)completableFuture.join(), (SpecialBlockModelRenderer)completableFuture2.join(), executor ); }, executor ) .thenCompose(reloadState -> reloadState.readyForUpload.thenApply(void_ -> reloadState)) .thenCompose(preparationBarrier::wait) .thenAcceptAsync(reloadState -> this.apply(reloadState, Profiler.get()), executor2); } private static CompletableFuture> loadBlockModels(ResourceManager resourceManager, Executor executor) { return CompletableFuture.supplyAsync(() -> MODEL_LISTER.listMatchingResources(resourceManager), executor) .thenCompose( map -> { List>> list = new ArrayList(map.size()); for (Entry entry : map.entrySet()) { list.add(CompletableFuture.supplyAsync(() -> { ResourceLocation resourceLocation = MODEL_LISTER.fileToId((ResourceLocation)entry.getKey()); try { Reader reader = ((Resource)entry.getValue()).openAsReader(); Pair var3; try { var3 = Pair.of(resourceLocation, BlockModel.fromStream(reader)); } catch (Throwable var6) { if (reader != null) { try { reader.close(); } catch (Throwable var5) { var6.addSuppressed(var5); } } throw var6; } if (reader != null) { reader.close(); } return var3; } catch (Exception var7) { LOGGER.error("Failed to load model {}", entry.getKey(), var7); return null; } }, executor)); } return Util.sequence(list) .thenApply(listx -> (Map)listx.stream().filter(Objects::nonNull).collect(Collectors.toUnmodifiableMap(Pair::getFirst, Pair::getSecond))); } ); } private static ModelManager.ResolvedModels discoverModelDependencies( Map inputModels, BlockStateModelLoader.LoadedModels loadedModels, ClientItemInfoLoader.LoadedClientInfos loadedClientInfos ) { ModelManager.ResolvedModels var5; try (Zone zone = Profiler.get().zone("dependencies")) { ModelDiscovery modelDiscovery = new ModelDiscovery(inputModels, MissingBlockModel.missingModel()); modelDiscovery.addSpecialModel(ItemModelGenerator.GENERATED_ITEM_MODEL_ID, new ItemModelGenerator()); loadedModels.models().values().forEach(modelDiscovery::addRoot); loadedClientInfos.contents().values().forEach(clientItem -> modelDiscovery.addRoot(clientItem.model())); var5 = new ModelManager.ResolvedModels(modelDiscovery.missingModel(), modelDiscovery.resolve()); } return var5; } private static CompletableFuture loadModels( Map stitchResults, ModelBakery modelBakery, Object2IntMap modelGroups, EntityModelSet entityModelSet, SpecialBlockModelRenderer specialBlockModelRenderer, Executor executor ) { CompletableFuture completableFuture = CompletableFuture.allOf( (CompletableFuture[])stitchResults.values().stream().map(AtlasSet.StitchResult::readyForUpload).toArray(CompletableFuture[]::new) ); final Multimap multimap = Multimaps.synchronizedMultimap(HashMultimap.create()); final Multimap multimap2 = Multimaps.synchronizedMultimap(HashMultimap.create()); return modelBakery.bakeModels(new SpriteGetter() { private final TextureAtlasSprite missingSprite = ((AtlasSet.StitchResult)stitchResults.get(TextureAtlas.LOCATION_BLOCKS)).missing(); @Override public TextureAtlasSprite get(Material material, ModelDebugName debugName) { AtlasSet.StitchResult stitchResult = (AtlasSet.StitchResult)stitchResults.get(material.atlasLocation()); TextureAtlasSprite textureAtlasSprite = stitchResult.getSprite(material.texture()); if (textureAtlasSprite != null) { return textureAtlasSprite; } else { multimap.put(debugName.debugName(), material); return stitchResult.missing(); } } @Override public TextureAtlasSprite reportMissingReference(String name, ModelDebugName debugName) { multimap2.put(debugName.debugName(), name); return this.missingSprite; } }, executor) .thenApply( bakingResult -> { multimap.asMap() .forEach( (string, collection) -> LOGGER.warn( "Missing textures in model {}:\n{}", string, collection.stream() .sorted(Material.COMPARATOR) .map(material -> " " + material.atlasLocation() + ":" + material.texture()) .collect(Collectors.joining("\n")) ) ); multimap2.asMap() .forEach( (string, collection) -> LOGGER.warn( "Missing texture references in model {}:\n{}", string, collection.stream().sorted().map(stringx -> " " + stringx).collect(Collectors.joining("\n")) ) ); Map map2 = createBlockStateToModelDispatch(bakingResult.blockStateModels(), bakingResult.missingModels().block()); return new ModelManager.ReloadState(bakingResult, modelGroups, map2, stitchResults, entityModelSet, specialBlockModelRenderer, completableFuture); } ); } private static Map createBlockStateToModelDispatch( Map blockStateModels, BlockStateModel missingModel ) { Object var8; try (Zone zone = Profiler.get().zone("block state dispatch")) { Map map = new IdentityHashMap(blockStateModels); for (Block block : BuiltInRegistries.BLOCK) { block.getStateDefinition().getPossibleStates().forEach(blockState -> { if (blockStateModels.putIfAbsent(blockState, missingModel) == null) { LOGGER.warn("Missing model for variant: '{}'", blockState); } }); } var8 = map; } return (Map)var8; } private static Object2IntMap buildModelGroups(BlockColors blockColors, BlockStateModelLoader.LoadedModels loadedModels) { Object2IntMap var3; try (Zone zone = Profiler.get().zone("block groups")) { var3 = ModelGroupCollector.build(blockColors, loadedModels); } return var3; } private void apply(ModelManager.ReloadState reloadState, ProfilerFiller profiler) { profiler.push("upload"); reloadState.atlasPreparations.values().forEach(AtlasSet.StitchResult::upload); ModelBakery.BakingResult bakingResult = reloadState.bakedModels; this.bakedItemStackModels = bakingResult.itemStackModels(); this.itemProperties = bakingResult.itemProperties(); this.modelGroups = reloadState.modelGroups; this.missingModels = bakingResult.missingModels(); profiler.popPush("cache"); this.blockModelShaper.replaceCache(reloadState.modelCache); this.specialBlockModelRenderer = reloadState.specialBlockModelRenderer; this.entityModelSet = reloadState.entityModelSet; profiler.pop(); } public boolean requiresRender(BlockState oldState, BlockState newState) { if (oldState == newState) { return false; } else { int i = this.modelGroups.getInt(oldState); if (i != -1) { int j = this.modelGroups.getInt(newState); if (i == j) { FluidState fluidState = oldState.getFluidState(); FluidState fluidState2 = newState.getFluidState(); return fluidState != fluidState2; } } return true; } } public TextureAtlas getAtlas(ResourceLocation location) { return this.atlases.getAtlas(location); } public void close() { this.atlases.close(); } public void updateMaxMipLevel(int level) { this.maxMipmapLevels = level; } public Supplier specialBlockModelRenderer() { return () -> this.specialBlockModelRenderer; } public Supplier entityModels() { return () -> this.entityModelSet; } @Environment(EnvType.CLIENT) record ReloadState( ModelBakery.BakingResult bakedModels, Object2IntMap modelGroups, Map modelCache, Map atlasPreparations, EntityModelSet entityModelSet, SpecialBlockModelRenderer specialBlockModelRenderer, CompletableFuture readyForUpload ) { } @Environment(EnvType.CLIENT) record ResolvedModels(ResolvedModel missing, Map models) { } }