package net.minecraft.client.renderer.block.model; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.mojang.datafixers.util.Either; import com.mojang.logging.LogUtils; import java.io.Reader; import java.lang.reflect.Type; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Map.Entry; import java.util.function.Function; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; import net.minecraft.client.renderer.texture.TextureAtlas; import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.BuiltInModel; import net.minecraft.client.resources.model.Material; import net.minecraft.client.resources.model.ModelBaker; import net.minecraft.client.resources.model.ModelState; import net.minecraft.client.resources.model.SimpleBakedModel; import net.minecraft.client.resources.model.SpecialModels; import net.minecraft.client.resources.model.UnbakedModel; import net.minecraft.core.Direction; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.GsonHelper; import net.minecraft.world.item.ItemDisplayContext; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @Environment(EnvType.CLIENT) public class BlockModel implements UnbakedModel { private static final Logger LOGGER = LogUtils.getLogger(); private static final FaceBakery FACE_BAKERY = new FaceBakery(); @VisibleForTesting static final Gson GSON = new GsonBuilder() .registerTypeAdapter(BlockModel.class, new BlockModel.Deserializer()) .registerTypeAdapter(BlockElement.class, new BlockElement.Deserializer()) .registerTypeAdapter(BlockElementFace.class, new BlockElementFace.Deserializer()) .registerTypeAdapter(BlockFaceUV.class, new BlockFaceUV.Deserializer()) .registerTypeAdapter(ItemTransform.class, new ItemTransform.Deserializer()) .registerTypeAdapter(ItemTransforms.class, new ItemTransforms.Deserializer()) .registerTypeAdapter(ItemOverride.class, new ItemOverride.Deserializer()) .create(); private static final char REFERENCE_CHAR = '#'; public static final String PARTICLE_TEXTURE_REFERENCE = "particle"; private static final boolean DEFAULT_AMBIENT_OCCLUSION = true; public static final Material MISSING_MATERIAL = new Material(TextureAtlas.LOCATION_BLOCKS, MissingTextureAtlasSprite.getLocation()); private final List elements; @Nullable private final BlockModel.GuiLight guiLight; @Nullable private final Boolean hasAmbientOcclusion; private final ItemTransforms transforms; private final List overrides; public String name = ""; @VisibleForTesting protected final Map> textureMap; @Nullable protected BlockModel parent; @Nullable protected ResourceLocation parentLocation; public static BlockModel fromStream(Reader reader) { return GsonHelper.fromJson(GSON, reader, BlockModel.class); } public BlockModel( @Nullable ResourceLocation parentLocation, List elements, Map> textureMap, @Nullable Boolean hasAmbientOcclusion, @Nullable BlockModel.GuiLight guiLight, ItemTransforms transforms, List overrides ) { this.elements = elements; this.hasAmbientOcclusion = hasAmbientOcclusion; this.guiLight = guiLight; this.textureMap = textureMap; this.parentLocation = parentLocation; this.transforms = transforms; this.overrides = overrides; } public List getElements() { return this.elements.isEmpty() && this.parent != null ? this.parent.getElements() : this.elements; } public boolean hasAmbientOcclusion() { if (this.hasAmbientOcclusion != null) { return this.hasAmbientOcclusion; } else { return this.parent != null ? this.parent.hasAmbientOcclusion() : true; } } public BlockModel.GuiLight getGuiLight() { if (this.guiLight != null) { return this.guiLight; } else { return this.parent != null ? this.parent.getGuiLight() : BlockModel.GuiLight.SIDE; } } public boolean isResolved() { return this.parentLocation == null || this.parent != null && this.parent.isResolved(); } public List getOverrides() { return this.overrides; } @Override public void resolveDependencies(UnbakedModel.Resolver resolver) { if (this.parentLocation != null) { if (!(resolver.resolve(this.parentLocation) instanceof BlockModel blockModel)) { throw new IllegalStateException("BlockModel parent has to be a block model."); } this.parent = blockModel; } } @Override public BakedModel bake(ModelBaker baker, Function spriteGetter, ModelState state) { return this.bake(spriteGetter, state, true); } public BakedModel bake(Function function, ModelState modelState, boolean bl) { TextureAtlasSprite textureAtlasSprite = (TextureAtlasSprite)function.apply(this.getMaterial("particle")); if (this.getRootModel() == SpecialModels.BLOCK_ENTITY_MARKER) { return new BuiltInModel(this.getTransforms(), textureAtlasSprite, this.getGuiLight().lightLikeBlock()); } else { SimpleBakedModel.Builder builder = new SimpleBakedModel.Builder(this, bl).particle(textureAtlasSprite); for (BlockElement blockElement : this.getElements()) { for (Direction direction : blockElement.faces.keySet()) { BlockElementFace blockElementFace = (BlockElementFace)blockElement.faces.get(direction); TextureAtlasSprite textureAtlasSprite2 = (TextureAtlasSprite)function.apply(this.getMaterial(blockElementFace.texture())); if (blockElementFace.cullForDirection() == null) { builder.addUnculledFace(bakeFace(blockElement, blockElementFace, textureAtlasSprite2, direction, modelState)); } else { builder.addCulledFace( Direction.rotate(modelState.getRotation().getMatrix(), blockElementFace.cullForDirection()), bakeFace(blockElement, blockElementFace, textureAtlasSprite2, direction, modelState) ); } } } return builder.build(); } } private static BakedQuad bakeFace(BlockElement element, BlockElementFace face, TextureAtlasSprite sprite, Direction facing, ModelState state) { return FACE_BAKERY.bakeQuad(element.from, element.to, face, sprite, facing, state, element.rotation, element.shade, element.lightEmission); } public boolean hasTexture(String textureName) { return !MissingTextureAtlasSprite.getLocation().equals(this.getMaterial(textureName).texture()); } public Material getMaterial(String name) { if (isTextureReference(name)) { name = name.substring(1); } List list = Lists.newArrayList(); while (true) { Either either = this.findTextureEntry(name); Optional optional = either.left(); if (optional.isPresent()) { return (Material)optional.get(); } name = (String)either.right().get(); if (list.contains(name)) { LOGGER.warn("Unable to resolve texture due to reference chain {}->{} in {}", Joiner.on("->").join(list), name, this.name); return MISSING_MATERIAL; } list.add(name); } } private Either findTextureEntry(String name) { for (BlockModel blockModel = this; blockModel != null; blockModel = blockModel.parent) { Either either = (Either)blockModel.textureMap.get(name); if (either != null) { return either; } } return Either.left(MISSING_MATERIAL); } static boolean isTextureReference(String str) { return str.charAt(0) == '#'; } public BlockModel getRootModel() { return this.parent == null ? this : this.parent.getRootModel(); } public ItemTransforms getTransforms() { ItemTransform itemTransform = this.getTransform(ItemDisplayContext.THIRD_PERSON_LEFT_HAND); ItemTransform itemTransform2 = this.getTransform(ItemDisplayContext.THIRD_PERSON_RIGHT_HAND); ItemTransform itemTransform3 = this.getTransform(ItemDisplayContext.FIRST_PERSON_LEFT_HAND); ItemTransform itemTransform4 = this.getTransform(ItemDisplayContext.FIRST_PERSON_RIGHT_HAND); ItemTransform itemTransform5 = this.getTransform(ItemDisplayContext.HEAD); ItemTransform itemTransform6 = this.getTransform(ItemDisplayContext.GUI); ItemTransform itemTransform7 = this.getTransform(ItemDisplayContext.GROUND); ItemTransform itemTransform8 = this.getTransform(ItemDisplayContext.FIXED); return new ItemTransforms(itemTransform, itemTransform2, itemTransform3, itemTransform4, itemTransform5, itemTransform6, itemTransform7, itemTransform8); } private ItemTransform getTransform(ItemDisplayContext displayContext) { return this.parent != null && !this.transforms.hasTransform(displayContext) ? this.parent.getTransform(displayContext) : this.transforms.getTransform(displayContext); } public String toString() { return this.name; } @Environment(EnvType.CLIENT) public static class Deserializer implements JsonDeserializer { public BlockModel deserialize(JsonElement json, Type type, JsonDeserializationContext context) throws JsonParseException { JsonObject jsonObject = json.getAsJsonObject(); List list = this.getElements(context, jsonObject); String string = this.getParentName(jsonObject); Map> map = this.getTextureMap(jsonObject); Boolean boolean_ = this.getAmbientOcclusion(jsonObject); ItemTransforms itemTransforms = ItemTransforms.NO_TRANSFORMS; if (jsonObject.has("display")) { JsonObject jsonObject2 = GsonHelper.getAsJsonObject(jsonObject, "display"); itemTransforms = context.deserialize(jsonObject2, ItemTransforms.class); } List list2 = this.getOverrides(context, jsonObject); BlockModel.GuiLight guiLight = null; if (jsonObject.has("gui_light")) { guiLight = BlockModel.GuiLight.getByName(GsonHelper.getAsString(jsonObject, "gui_light")); } ResourceLocation resourceLocation = string.isEmpty() ? null : ResourceLocation.parse(string); return new BlockModel(resourceLocation, list, map, boolean_, guiLight, itemTransforms, list2); } protected List getOverrides(JsonDeserializationContext context, JsonObject json) { List list = Lists.newArrayList(); if (json.has("overrides")) { for (JsonElement jsonElement : GsonHelper.getAsJsonArray(json, "overrides")) { list.add((ItemOverride)context.deserialize(jsonElement, ItemOverride.class)); } } return list; } private Map> getTextureMap(JsonObject json) { ResourceLocation resourceLocation = TextureAtlas.LOCATION_BLOCKS; Map> map = Maps.>newHashMap(); if (json.has("textures")) { JsonObject jsonObject = GsonHelper.getAsJsonObject(json, "textures"); for (Entry entry : jsonObject.entrySet()) { map.put((String)entry.getKey(), parseTextureLocationOrReference(resourceLocation, ((JsonElement)entry.getValue()).getAsString())); } } return map; } private static Either parseTextureLocationOrReference(ResourceLocation location, String name) { if (BlockModel.isTextureReference(name)) { return Either.right(name.substring(1)); } else { ResourceLocation resourceLocation = ResourceLocation.tryParse(name); if (resourceLocation == null) { throw new JsonParseException(name + " is not valid resource location"); } else { return Either.left(new Material(location, resourceLocation)); } } } private String getParentName(JsonObject json) { return GsonHelper.getAsString(json, "parent", ""); } @Nullable protected Boolean getAmbientOcclusion(JsonObject json) { return json.has("ambientocclusion") ? GsonHelper.getAsBoolean(json, "ambientocclusion") : null; } protected List getElements(JsonDeserializationContext context, JsonObject json) { List list = Lists.newArrayList(); if (json.has("elements")) { for (JsonElement jsonElement : GsonHelper.getAsJsonArray(json, "elements")) { list.add((BlockElement)context.deserialize(jsonElement, BlockElement.class)); } } return list; } } @Environment(EnvType.CLIENT) public static enum GuiLight { FRONT("front"), SIDE("side"); private final String name; private GuiLight(final String name) { this.name = name; } public static BlockModel.GuiLight getByName(String name) { for (BlockModel.GuiLight guiLight : values()) { if (guiLight.name.equals(name)) { return guiLight; } } throw new IllegalArgumentException("Invalid gui light: " + name); } public boolean lightLikeBlock() { return this == SIDE; } } }