/* * Minecraft Forge * Copyright (c) 2016-2018. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.client.model; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.concurrent.TimeUnit; import net.minecraft.block.Block; import net.minecraft.block.state.IBlockState; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.BlockModelShapes; import net.minecraft.client.renderer.ItemMeshDefinition; import net.minecraft.client.renderer.ItemModelMesher; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.client.renderer.block.model.BlockPart; import net.minecraft.client.renderer.block.model.BlockPartFace; import net.minecraft.client.renderer.block.model.BlockPartRotation; import net.minecraft.client.renderer.block.model.BuiltInModel; import net.minecraft.client.renderer.block.model.IBakedModel; import net.minecraft.client.renderer.block.model.ItemCameraTransforms; import net.minecraft.client.renderer.block.model.ItemCameraTransforms.TransformType; import net.minecraft.client.renderer.block.model.ItemModelGenerator; import net.minecraft.client.renderer.block.model.ItemOverrideList; import net.minecraft.client.renderer.block.model.ModelBakery; import net.minecraft.client.renderer.block.model.ModelBlock; import net.minecraft.client.renderer.block.model.ModelBlockDefinition; import net.minecraft.client.renderer.block.model.ModelBlockDefinition.MissingVariantException; import net.minecraft.client.renderer.block.model.ModelResourceLocation; import net.minecraft.client.renderer.block.model.MultipartBakedModel; import net.minecraft.client.renderer.block.model.SimpleBakedModel; import net.minecraft.client.renderer.block.model.Variant; import net.minecraft.client.renderer.block.model.VariantList; import net.minecraft.client.renderer.block.model.WeightedBakedModel; import net.minecraft.client.renderer.block.model.multipart.Multipart; import net.minecraft.client.renderer.block.model.multipart.Selector; import net.minecraft.client.renderer.block.statemap.BlockStateMapper; import net.minecraft.client.renderer.block.statemap.IStateMapper; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureMap; import net.minecraft.client.renderer.vertex.DefaultVertexFormats; import net.minecraft.client.renderer.vertex.VertexFormat; import net.minecraft.client.resources.IResourceManager; import net.minecraft.item.Item; import net.minecraft.util.EnumFacing; import net.minecraft.util.ResourceLocation; import net.minecraft.util.registry.IRegistry; import net.minecraftforge.client.model.animation.AnimationItemOverrideList; import net.minecraftforge.client.model.animation.ModelBlockAnimation; import net.minecraftforge.common.ForgeMod; import net.minecraftforge.common.model.IModelState; import net.minecraftforge.common.model.Models; import net.minecraftforge.common.model.TRSRTransformation; import net.minecraftforge.common.model.animation.IClip; import net.minecraftforge.common.property.IExtendedBlockState; import net.minecraftforge.common.property.Properties; import net.minecraftforge.fluids.FluidRegistry; import net.minecraftforge.fml.client.ClientModLoader; import net.minecraftforge.fml.common.FMLLog; import net.minecraftforge.fml.common.ProgressManager; import net.minecraftforge.fml.common.ProgressManager.ProgressBar; import net.minecraftforge.logging.ModelLoaderErrorMessage; import net.minecraftforge.registries.IRegistryDelegate; import org.apache.commons.lang3.tuple.Pair; import java.util.function.Function; import com.google.common.base.Joiner; import com.google.common.base.Objects; import com.google.common.base.Preconditions; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimaps; import com.google.common.collect.Sets; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import static net.minecraftforge.fml.Logging.MODELLOADING; public final class ModelLoader extends ModelBakery { private static final Logger LOGGER = LogManager.getLogger(); private final Map stateModels = Maps.newHashMap(); private final Map multipartDefinitions = Maps.newHashMap(); private final Map multipartModels = Maps.newHashMap(); // TODO: nothing adds to missingVariants, remove it? private final Set missingVariants = Sets.newHashSet(); private final Map loadingExceptions = Maps.newHashMap(); private IModel missingModel = null; private boolean isLoading = false; public boolean isLoading() { return isLoading; } public ModelLoader(IResourceManager manager, TextureMap map, BlockModelShapes shapes) { super(manager, map, shapes); VanillaLoader.INSTANCE.setLoader(this); VariantLoader.INSTANCE.setLoader(this); ModelLoaderRegistry.clearModelCache(manager); } @Nonnull @Override public IRegistry setupModelRegistry() { if (ClientModLoader.isErrored()) // skip loading models if we're just going to show a fatal error screen return bakedRegistry; isLoading = true; loadBlocks(); loadVariantItemModels(); missingModel = ModelLoaderRegistry.getMissingModel(); stateModels.put(MODEL_MISSING, missingModel); final Set textures = Sets.newHashSet(ModelLoaderRegistry.getTextures()); textures.remove(TextureMap.LOCATION_MISSING_TEXTURE); textures.addAll(LOCATIONS_BUILTIN_TEXTURES); textureMap.loadSprites(resourceManager, map -> textures.forEach(map::registerSprite)); IBakedModel missingBaked = missingModel.bake(missingModel.getDefaultState(), DefaultVertexFormats.ITEM, DefaultTextureGetter.INSTANCE); Map bakedModels = Maps.newHashMap(); HashMultimap models = HashMultimap.create(); Multimaps.invertFrom(Multimaps.forMap(stateModels), models); ProgressBar bakeBar = ProgressManager.push("ModelLoader: baking", models.keySet().size()); for(IModel model : models.keySet()) { String modelLocations = "[" + Joiner.on(", ").join(models.get(model)) + "]"; bakeBar.step(modelLocations); if(model == getMissingModel()) { bakedModels.put(model, missingBaked); } else { try { bakedModels.put(model, model.bake(model.getDefaultState(), DefaultVertexFormats.ITEM, DefaultTextureGetter.INSTANCE)); } catch (Exception e) { FMLLog.log.error("Exception baking model for location(s) {}:", modelLocations, e); bakedModels.put(model, missingBaked); } } } ProgressManager.pop(bakeBar); for (Entry e : stateModels.entrySet()) { bakedRegistry.putObject(e.getKey(), bakedModels.get(e.getValue())); } return bakedRegistry; } // NOOP, replaced by dependency resolution @Override protected void loadVariantModels() {} // NOOP, replaced by dependency resolution @Override protected void loadMultipartVariantModels() {} @Override protected void loadBlocks() { List blocks = StreamSupport.stream(Block.REGISTRY.spliterator(), false) .filter(block -> block.getRegistryName() != null) .sorted(Comparator.comparing(b -> b.getRegistryName().toString())) .collect(Collectors.toList()); ProgressBar blockBar = ProgressManager.push("ModelLoader: blocks", blocks.size()); BlockStateMapper mapper = this.blockModelShapes.getBlockStateMapper(); for(Block block : blocks) { blockBar.step(block.getRegistryName().toString()); for(ResourceLocation location : mapper.getBlockstateLocations(block)) { loadBlock(mapper, block, location); } } ProgressManager.pop(blockBar); } @Override protected void registerVariant(@Nullable ModelBlockDefinition definition, ModelResourceLocation location) { IModel model; try { model = ModelLoaderRegistry.getModel(location); } catch(Exception e) { storeException(location, e); model = ModelLoaderRegistry.getMissingModel(location, e); } stateModels.put(location, model); } @Override protected void registerMultipartVariant(ModelBlockDefinition definition, Collection locations) { for (ModelResourceLocation location : locations) { multipartDefinitions.put(location, definition); registerVariant(null, location); } } private void storeException(ResourceLocation location, Exception exception) { loadingExceptions.put(location, exception); } @Override protected ModelBlockDefinition getModelBlockDefinition(ResourceLocation location) { try { return super.getModelBlockDefinition(location); } catch (Exception exception) { storeException(location, new Exception("Could not load model definition for variant " + location, exception)); } return new ModelBlockDefinition(new ArrayList<>()); } @Override protected void loadItemModels() { registerVariantNames(); List items = StreamSupport.stream(Item.REGISTRY.spliterator(), false) .filter(item -> item.getRegistryName() != null) .sorted(Comparator.comparing(i -> i.getRegistryName().toString())) .collect(Collectors.toList()); ProgressBar itemBar = ProgressManager.push("ModelLoader: items", items.size()); for(Item item : items) { itemBar.step(item.getRegistryName().toString()); for(String s : getVariantNames(item)) { ResourceLocation file = getItemLocation(s); ModelResourceLocation memory = getInventoryVariant(s); IModel model = ModelLoaderRegistry.getMissingModel(); Exception exception = null; try { model = ModelLoaderRegistry.getModel(memory); } catch (Exception blockstateException) { try { model = ModelLoaderRegistry.getModel(file); ModelLoaderRegistry.addAlias(memory, file); } catch (Exception normalException) { exception = new ItemLoadingException("Could not load item model either from the normal location " + file + " or from the blockstate", normalException, blockstateException); } } if (exception != null) { storeException(memory, exception); model = ModelLoaderRegistry.getMissingModel(memory, exception); } stateModels.put(memory, model); } } ProgressManager.pop(itemBar); } /** * Hooked from ModelBakery, allows using MRLs that don't end with "inventory" for items. */ public static ModelResourceLocation getInventoryVariant(String s) { if(s.contains("#")) { return new ModelResourceLocation(s); } return new ModelResourceLocation(s, "inventory"); } @Override protected ResourceLocation getModelLocation(ResourceLocation model) { return new ResourceLocation(model.getResourceDomain(), model.getResourcePath() + ".json"); } private final class VanillaModelWrapper implements IModel { private final ResourceLocation location; private final ModelBlock model; private final boolean uvlock; private final ModelBlockAnimation animation; public VanillaModelWrapper(ResourceLocation location, ModelBlock model, boolean uvlock, ModelBlockAnimation animation) { this.location = location; this.model = model; this.uvlock = uvlock; this.animation = animation; } @Override public Collection getDependencies() { Set set = Sets.newHashSet(); for(ResourceLocation dep : model.getOverrideLocations()) { if(!location.equals(dep)) { set.add(dep); // TODO: check if this can go somewhere else, random access to global things is bad stateModels.put(getInventoryVariant(dep.toString()), ModelLoaderRegistry.getModelOrLogError(dep, "Could not load override model " + dep + " for model " + location)); } } if(model.getParentLocation() != null && !model.getParentLocation().getResourcePath().startsWith("builtin/")) { set.add(model.getParentLocation()); } return ImmutableSet.copyOf(set); } @Override public Collection getTextures() { // setting parent here to make textures resolve properly if(model.getParentLocation() != null) { if(model.getParentLocation().getResourcePath().equals("builtin/generated")) { model.parent = MODEL_GENERATED; } else { IModel parent = ModelLoaderRegistry.getModelOrLogError(model.getParentLocation(), "Could not load vanilla model parent '" + model.getParentLocation() + "' for '" + model); if(parent instanceof VanillaModelWrapper) { model.parent = ((VanillaModelWrapper) parent).model; } else { throw new IllegalStateException("vanilla model '" + model + "' can't have non-vanilla parent"); } } } ImmutableSet.Builder builder = ImmutableSet.builder(); if(hasItemModel(model)) { for(String s : ItemModelGenerator.LAYERS) { String r = model.resolveTextureName(s); ResourceLocation loc = new ResourceLocation(r); if(!r.equals(s)) { builder.add(loc); } } } for(String s : model.textures.values()) { if(!s.startsWith("#")) { builder.add(new ResourceLocation(s)); } } return builder.build(); } @Override public IBakedModel bake(IModelState state, VertexFormat format, Function bakedTextureGetter) { return VanillaLoader.INSTANCE.modelCache.getUnchecked(new BakedModelCacheKey(this, state, format, bakedTextureGetter)); } public IBakedModel bakeImpl(IModelState state, final VertexFormat format, Function bakedTextureGetter) { if(!Attributes.moreSpecific(format, Attributes.DEFAULT_BAKED_FORMAT)) { throw new IllegalArgumentException("can't bake vanilla models to the format that doesn't fit into the default one: " + format); } ModelBlock model = this.model; if(model == null) return getMissingModel().bake(getMissingModel().getDefaultState(), format, bakedTextureGetter); List newTransforms = Lists.newArrayList(); for(int i = 0; i < model.getElements().size(); i++) { BlockPart part = model.getElements().get(i); newTransforms.add(animation.getPartTransform(state, part, i)); } ItemCameraTransforms transforms = model.getAllTransforms(); Map tMap = Maps.newEnumMap(TransformType.class); tMap.putAll(PerspectiveMapWrapper.getTransforms(transforms)); tMap.putAll(PerspectiveMapWrapper.getTransforms(state)); IModelState perState = new SimpleModelState(ImmutableMap.copyOf(tMap)); if(hasItemModel(model)) { return new ItemLayerModel(model).bake(perState, format, bakedTextureGetter); } if(isCustomRenderer(model)) return new BuiltInModel(transforms, model.createOverrides()); return bakeNormal(model, perState, state, newTransforms, format, bakedTextureGetter, uvlock); } private IBakedModel bakeNormal(ModelBlock model, IModelState perState, final IModelState modelState, List newTransforms, final VertexFormat format, final Function bakedTextureGetter, boolean uvLocked) { final TRSRTransformation baseState = modelState.apply(Optional.empty()).orElse(TRSRTransformation.identity()); TextureAtlasSprite particle = bakedTextureGetter.apply(new ResourceLocation(model.resolveTextureName("particle"))); SimpleBakedModel.Builder builder = (new SimpleBakedModel.Builder(model, model.createOverrides())).setTexture(particle); for(int i = 0; i < model.getElements().size(); i++) { if(modelState.apply(Optional.of(Models.getHiddenModelPart(ImmutableList.of(Integer.toString(i))))).isPresent()) { continue; } BlockPart part = model.getElements().get(i); TRSRTransformation transformation = baseState; if(newTransforms.get(i) != null) { transformation = transformation.compose(newTransforms.get(i)); BlockPartRotation rot = part.partRotation; if(rot == null) rot = new BlockPartRotation(new org.lwjgl.util.vector.Vector3f(), EnumFacing.Axis.Y, 0, false); part = new BlockPart(part.positionFrom, part.positionTo, part.mapFaces, rot, part.shade); } for(Map.Entry e : part.mapFaces.entrySet()) { TextureAtlasSprite textureatlassprite1 = bakedTextureGetter.apply(new ResourceLocation(model.resolveTextureName(e.getValue().texture))); if (e.getValue().cullFace == null || !TRSRTransformation.isInteger(transformation.getMatrix())) { builder.addGeneralQuad(makeBakedQuad(part, e.getValue(), textureatlassprite1, e.getKey(), transformation, uvLocked)); } else { builder.addFaceQuad(baseState.rotate(e.getValue().cullFace), makeBakedQuad(part, e.getValue(), textureatlassprite1, e.getKey(), transformation, uvLocked)); } } } return new PerspectiveMapWrapper(builder.makeBakedModel(), perState) { private final ItemOverrideList overrides = new AnimationItemOverrideList(VanillaModelWrapper.this, modelState, format, bakedTextureGetter, super.getOverrides()); @Override public List getQuads(@Nullable IBlockState state, @Nullable EnumFacing side, long rand) { if(state instanceof IExtendedBlockState) { IExtendedBlockState exState = (IExtendedBlockState)state; if(exState.getUnlistedNames().contains(Properties.AnimationProperty)) { IModelState newState = exState.getValue(Properties.AnimationProperty); IExtendedBlockState newExState = exState.withProperty(Properties.AnimationProperty, null); if(newState != null) { return VanillaModelWrapper.this.bake(new ModelStateComposition(modelState, newState), format, bakedTextureGetter).getQuads(newExState, side, rand); } } } return super.getQuads(state, side, rand); } @Override public ItemOverrideList getOverrides() { return overrides; } }; } @Override public VanillaModelWrapper retexture(ImmutableMap textures) { if (textures.isEmpty()) return this; List elements = Lists.newArrayList(); //We have to duplicate this so we can edit it below. for (BlockPart part : this.model.getElements()) { elements.add(new BlockPart(part.positionFrom, part.positionTo, Maps.newHashMap(part.mapFaces), part.partRotation, part.shade)); } ModelBlock newModel = new ModelBlock(this.model.getParentLocation(), elements, Maps.newHashMap(this.model.textures), this.model.isAmbientOcclusion(), this.model.isGui3d(), //New Textures man VERY IMPORTANT model.getAllTransforms(), Lists.newArrayList(model.getOverrides())); newModel.name = this.model.name; newModel.parent = this.model.parent; Set removed = Sets.newHashSet(); for (Entry e : textures.entrySet()) { if ("".equals(e.getValue())) { removed.add(e.getKey()); newModel.textures.remove(e.getKey()); } else newModel.textures.put(e.getKey(), e.getValue()); } // Map the model's texture references as if it was the parent of a model with the retexture map as its textures. Map remapped = Maps.newHashMap(); for (Entry e : newModel.textures.entrySet()) { if (e.getValue().startsWith("#")) { String key = e.getValue().substring(1); if (newModel.textures.containsKey(key)) remapped.put(e.getKey(), newModel.textures.get(key)); } } newModel.textures.putAll(remapped); //Remove any faces that use a null texture, this is for performance reasons, also allows some cool layering stuff. for (BlockPart part : newModel.getElements()) { part.mapFaces.entrySet().removeIf(entry -> removed.contains(entry.getValue().texture)); } return new VanillaModelWrapper(location, newModel, uvlock, animation); } @Override public Optional getClip(String name) { if(animation.getClips().containsKey(name)) { return Optional.ofNullable(animation.getClips().get(name)); } return Optional.empty(); } @Override public VanillaModelWrapper smoothLighting(boolean value) { if(model.ambientOcclusion == value) { return this; } ModelBlock newModel = new ModelBlock(model.getParentLocation(), model.getElements(), model.textures, value, model.isGui3d(), model.getAllTransforms(), Lists.newArrayList(model.getOverrides())); newModel.parent = model.parent; newModel.name = model.name; return new VanillaModelWrapper(location, newModel, uvlock, animation); } @Override public VanillaModelWrapper gui3d(boolean value) { if(model.isGui3d() == value) { return this; } ModelBlock newModel = new ModelBlock(model.getParentLocation(), model.getElements(), model.textures, model.ambientOcclusion, value, model.getAllTransforms(), Lists.newArrayList(model.getOverrides())); newModel.parent = model.parent; newModel.name = model.name; return new VanillaModelWrapper(location, newModel, uvlock, animation); } @Override public IModel uvlock(boolean value) { if(uvlock == value) { return this; } return new VanillaModelWrapper(location, model, value, animation); } } private static final class WeightedRandomModel implements IModel { private final List variants; private final List locations; private final Set textures; private final List models; private final IModelState defaultState; public WeightedRandomModel(ResourceLocation parent, VariantList variants) throws Exception { this.variants = variants.getVariantList(); this.locations = new ArrayList<>(); this.textures = Sets.newHashSet(); this.models = new ArrayList<>(); ImmutableList.Builder> builder = ImmutableList.builder(); for (Variant v : this.variants) { ResourceLocation loc = v.getModelLocation(); locations.add(loc); /* * Vanilla eats this, which makes it only show variants that have models. * But that doesn't help debugging, so throw the exception */ IModel model; if(loc.equals(MODEL_MISSING)) { // explicit missing location, happens if blockstate has "model"=null model = ModelLoaderRegistry.getMissingModel(); } else { model = ModelLoaderRegistry.getModel(loc); } // FIXME: is this the place? messes up dependency and texture resolution model = v.process(model); for(ResourceLocation location : model.getDependencies()) { ModelLoaderRegistry.getModelOrMissing(location); } //FMLLog.getLogger().error("Exception resolving indirect dependencies for model" + loc, e); textures.addAll(model.getTextures()); // Kick this, just in case. models.add(model); IModelState modelDefaultState = model.getDefaultState(); Preconditions.checkNotNull(modelDefaultState, "Model %s returned null as default state", loc); builder.add(Pair.of(model, new ModelStateComposition(v.getState(), modelDefaultState))); } if (models.size() == 0) //If all variants are missing, add one with the missing model and default rotation. { // FIXME: log this? IModel missing = ModelLoaderRegistry.getMissingModel(); models.add(missing); builder.add(Pair.of(missing, TRSRTransformation.identity())); } defaultState = new MultiModelState(builder.build()); } private WeightedRandomModel(List variants, List locations, Set textures, List models, IModelState defaultState) { this.variants = variants; this.locations = locations; this.textures = textures; this.models = models; this.defaultState = defaultState; } @Override public Collection getDependencies() { return ImmutableList.copyOf(locations); } @Override public Collection getTextures() { return ImmutableSet.copyOf(textures); } @Override public IBakedModel bake(IModelState state, VertexFormat format, Function bakedTextureGetter) { if(!Attributes.moreSpecific(format, Attributes.DEFAULT_BAKED_FORMAT)) { throw new IllegalArgumentException("can't bake vanilla weighted models to the format that doesn't fit into the default one: " + format); } if(variants.size() == 1) { IModel model = models.get(0); return model.bake(MultiModelState.getPartState(state, model, 0), format, bakedTextureGetter); } WeightedBakedModel.Builder builder = new WeightedBakedModel.Builder(); for(int i = 0; i < variants.size(); i++) { IModel model = models.get(i); builder.add(model.bake(MultiModelState.getPartState(state, model, i), format, bakedTextureGetter), variants.get(i).getWeight()); } return builder.build(); } @Override public IModelState getDefaultState() { return defaultState; } @Override public WeightedRandomModel retexture(ImmutableMap textures) { if (textures.isEmpty()) return this; // rebuild the texture list taking into account new textures Set modelTextures = Sets.newHashSet(); // also recreate the MultiModelState so IModelState data is properly applied to the retextured model ImmutableList.Builder> builder = ImmutableList.builder(); List retexturedModels = Lists.newArrayList(); for(int i = 0; i < this.variants.size(); i++) { IModel retextured = this.models.get(i).retexture(textures); modelTextures.addAll(retextured.getTextures()); retexturedModels.add(retextured); builder.add(Pair.of(retextured, this.variants.get(i).getState())); } return new WeightedRandomModel(this.variants, this.locations, modelTextures, retexturedModels, new MultiModelState(builder.build())); } } protected IModel getMissingModel() { if (missingModel == null) { try { missingModel = VanillaLoader.INSTANCE.loadModel(new ResourceLocation(MODEL_MISSING.getResourceDomain(), MODEL_MISSING.getResourcePath())); } catch(Exception e) { throw new RuntimeException("Missing the missing model, this should never happen"); } } return missingModel; } protected final class BakedModelCacheKey { private final VanillaModelWrapper model; private final IModelState state; private final VertexFormat format; private final Function bakedTextureGetter; public BakedModelCacheKey(VanillaModelWrapper model, IModelState state, VertexFormat format, Function bakedTextureGetter) { this.model = model; this.state = state; this.format = format; this.bakedTextureGetter = bakedTextureGetter; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BakedModelCacheKey that = (BakedModelCacheKey) o; return Objects.equal(model, that.model) && Objects.equal(state, that.state) && Objects.equal(format, that.format) && Objects.equal(bakedTextureGetter, that.bakedTextureGetter); } @Override public int hashCode() { return Objects.hashCode(model, state, format, bakedTextureGetter); } } protected static enum VanillaLoader implements ICustomModelLoader { INSTANCE; @Nullable private ModelLoader loader; private LoadingCache modelCache = CacheBuilder.newBuilder().maximumSize(50).expireAfterWrite(100, TimeUnit.MILLISECONDS).build(new CacheLoader() { @Override public IBakedModel load(BakedModelCacheKey key) throws Exception { return key.model.bakeImpl(key.state, key.format, key.bakedTextureGetter); } }); void setLoader(ModelLoader loader) { this.loader = loader; } @Nullable ModelLoader getLoader() { return loader; } // NOOP, handled in loader @Override public void onResourceManagerReload(IResourceManager resourceManager) {} @Override public boolean accepts(ResourceLocation modelLocation) { return true; } @Override public IModel loadModel(ResourceLocation modelLocation) throws Exception { if(modelLocation.equals(MODEL_MISSING) && loader.missingModel != null) { return loader.getMissingModel(); } String modelPath = modelLocation.getResourcePath(); if(modelLocation.getResourcePath().startsWith("models/")) { modelPath = modelPath.substring("models/".length()); } ResourceLocation armatureLocation = new ResourceLocation(modelLocation.getResourceDomain(), "armatures/" + modelPath + ".json"); ModelBlockAnimation animation = ModelBlockAnimation.loadVanillaAnimation(loader.resourceManager, armatureLocation); ModelBlock model = loader.loadModel(modelLocation); IModel iModel = loader.new VanillaModelWrapper(modelLocation, model, false, animation); if(loader.missingModel == null && modelLocation.equals(MODEL_MISSING)) { loader.missingModel = iModel; } return iModel; } @Override public String toString() { return "VanillaLoader.INSTANCE"; } } /** * 16x16 pure white sprite. */ public static final class White extends TextureAtlasSprite { public static final ResourceLocation LOCATION = new ResourceLocation("white"); public static final White INSTANCE = new White(); private White() { super(LOCATION.toString()); this.width = this.height = 16; } @Override public boolean hasCustomLoader(IResourceManager manager, ResourceLocation location) { return true; } @Override public boolean load(IResourceManager manager, ResourceLocation location, Function textureGetter) { BufferedImage image = new BufferedImage(this.getIconWidth(), this.getIconHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D graphics = image.createGraphics(); graphics.setBackground(Color.WHITE); graphics.clearRect(0, 0, this.getIconWidth(), this.getIconHeight()); int[][] pixels = new int[Minecraft.getMinecraft().gameSettings.mipmapLevels + 1][]; pixels[0] = new int[image.getWidth() * image.getHeight()]; image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels[0], 0, image.getWidth()); this.clearFramesTextureData(); this.framesTextureData.add(pixels); return false; } public void register(TextureMap map) { map.setTextureEntry(White.INSTANCE); } } public static class ItemLoadingException extends ModelLoaderRegistry.LoaderException { private final Exception normalException; private final Exception blockstateException; public ItemLoadingException(String message, Exception normalException, Exception blockstateException) { super(message); this.normalException = normalException; this.blockstateException = blockstateException; } } /** * Internal, do not use. */ public void onPostBakeEvent(IRegistry modelRegistry) { IBakedModel missingModel = modelRegistry.getObject(MODEL_MISSING); for(Map.Entry entry : loadingExceptions.entrySet()) { // ignoring pure ResourceLocation arguments, all things we care about pass ModelResourceLocation if(entry.getKey() instanceof ModelResourceLocation) { LOGGER.debug(MODELLOADING, ()-> new ModelLoaderErrorMessage((ModelResourceLocation)entry.getKey(), entry.getValue(), modelRegistry, this.blockModelShapes, this::getVariantNames)); final ModelResourceLocation location = (ModelResourceLocation)entry.getKey(); final IBakedModel model = modelRegistry.getObject(location); if(model == null) { modelRegistry.putObject(location, missingModel); } } } for(ModelResourceLocation missing : missingVariants) { IBakedModel model = modelRegistry.getObject(missing); if(model == null || model == missingModel) { LOGGER.debug(MODELLOADING, ()-> new ModelLoaderErrorMessage(missing, null, modelRegistry, this.blockModelShapes, this::getVariantNames)); } if(model == null) { modelRegistry.putObject(missing, missingModel); } } loadingExceptions.clear(); missingVariants.clear(); isLoading = false; } private static final Map, IStateMapper> customStateMappers = Maps.newHashMap(); /** * Adds a custom IBlockState -> model variant logic. */ public static void setCustomStateMapper(Block block, IStateMapper mapper) { customStateMappers.put(block.delegate, mapper); } /** * Internal, do not use. */ public static void onRegisterAllBlocks(BlockModelShapes shapes) { for (Entry, IStateMapper> e : customStateMappers.entrySet()) { shapes.registerBlockWithStateMapper(e.getKey().get(), e.getValue()); } } private static final Map, ItemMeshDefinition> customMeshDefinitions = com.google.common.collect.Maps.newHashMap(); private static final Map, Integer>, ModelResourceLocation> customModels = com.google.common.collect.Maps.newHashMap(); /** * Adds a simple mapping from Item + metadata to the model variant. * Registers the variant with the ModelBakery too. */ public static void setCustomModelResourceLocation(Item item, int metadata, ModelResourceLocation model) { customModels.put(Pair.of(item.delegate, metadata), model); ModelBakery.registerItemVariants(item, model); } /** * Adds generic ItemStack -> model variant logic. * You still need to manually call ModelBakery.registerItemVariants with all values that meshDefinition can return. */ public static void setCustomMeshDefinition(Item item, ItemMeshDefinition meshDefinition) { customMeshDefinitions.put(item.delegate, meshDefinition); } /** * Helper method for registering all itemstacks for given item to map to universal bucket model. */ public static void setBucketModelDefinition(Item item) { ModelLoader.setCustomMeshDefinition(item, stack -> ModelDynBucket.LOCATION); ModelBakery.registerItemVariants(item, ModelDynBucket.LOCATION); } /** * Internal, do not use. */ public static void onRegisterItems(ItemModelMesher mesher) { for (Map.Entry, ItemMeshDefinition> e : customMeshDefinitions.entrySet()) { mesher.register(e.getKey().get(), e.getValue()); } for (Entry, Integer>, ModelResourceLocation> e : customModels.entrySet()) { mesher.register(e.getKey().getLeft().get(), e.getKey().getRight(), e.getValue()); } } private static enum DefaultTextureGetter implements Function { INSTANCE; @Override public TextureAtlasSprite apply(ResourceLocation location) { return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(location.toString()); } } /** * Get the default texture getter the models will be baked with. */ public static Function defaultTextureGetter() { return DefaultTextureGetter.INSTANCE; } protected static enum VariantLoader implements ICustomModelLoader { INSTANCE; private ModelLoader loader; void setLoader(ModelLoader loader) { this.loader = loader; } // NOOP, handled in loader @Override public void onResourceManagerReload(IResourceManager resourceManager) {} @Override public boolean accepts(ResourceLocation modelLocation) { return modelLocation instanceof ModelResourceLocation; } @Override public IModel loadModel(ResourceLocation modelLocation) throws Exception { ModelResourceLocation variant = (ModelResourceLocation) modelLocation; ModelBlockDefinition definition = loader.getModelBlockDefinition(variant); try { VariantList variants = definition.getVariant(variant.getVariant()); return new WeightedRandomModel(variant, variants); } catch (MissingVariantException e) { if (definition.equals(loader.multipartDefinitions.get(variant))) { IModel model = loader.multipartModels.get(definition); if (model == null) { model = new MultipartModel(new ResourceLocation(variant.getResourceDomain(), variant.getResourcePath()), definition.getMultipartData()); loader.multipartModels.put(definition, model); } return model; } throw e; } } @Override public String toString() { return "VariantLoader.INSTANCE"; } } private static class MultipartModel implements IModel { private final ResourceLocation location; private final Multipart multipart; private final ImmutableMap partModels; public MultipartModel(ResourceLocation location, Multipart multipart) throws Exception { this.location = location; this.multipart = multipart; ImmutableMap.Builder builder = ImmutableMap.builder(); for (Selector selector : multipart.getSelectors()) { builder.put(selector, new WeightedRandomModel(location, selector.getVariantList())); } partModels = builder.build(); } private MultipartModel(ResourceLocation location, Multipart multipart, ImmutableMap partModels) { this.location = location; this.multipart = multipart; this.partModels = partModels; } // FIXME: represent selectors as dependencies? // FIXME @Override public IBakedModel bake(IModelState state, VertexFormat format, Function bakedTextureGetter) { MultipartBakedModel.Builder builder = new MultipartBakedModel.Builder(); for (Selector selector : multipart.getSelectors()) { builder.putModel(selector.getPredicate(multipart.getStateContainer()), partModels.get(selector).bake(partModels.get(selector).getDefaultState(), format, bakedTextureGetter)); } IBakedModel bakedModel = builder.makeMultipartModel(); return bakedModel; } @Override public IModel retexture(ImmutableMap textures) { if (textures.isEmpty()) return this; ImmutableMap.Builder builder = ImmutableMap.builder(); for (Entry partModel : this.partModels.entrySet()) { builder.put(partModel.getKey(), partModel.getValue().retexture(textures)); } return new MultipartModel(location, multipart, builder.build()); } } }