Repository: FabricMC/fabric (Java)

Issue:
ModelLoadingEventDispatcher.BlockStateResolverContext#setModel() does not null-check the given blockstate
`ModelLoadingEventDispatcher.BlockStateResolverContext#setModel()` null-checks the given model a second time instead of null-checking the given blockstate: https://github.com/FabricMC/fabric/blob/d70d2c06bb8fafdb72c6778b29fb050618015ab3/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java#L180-L182

FAPI version: 0.112.2+1.21.4



Fix (patch):
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
index 30bc4ea274..a9138c92e2 100644
--- a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
@@ -82,12 +82,38 @@ interface Context {
 
 		/**
 		 * Event access to monitor unbaked model loads and replace the loaded model.
+		 *
+		 * <p>Replacements done by listeners of this callback <b>do</b> affect child models (that is, models whose
+		 * parent hierarchy contains the replaced model), unlike {@link #modifyModelBeforeBake}.
 		 */
 		Event<ModelModifier.OnLoad> modifyModelOnLoad();
 
+		/**
+		 * Event access to replace the unbaked model used for baking without replacing the cached model.
+		 *
+		 * <p>Replacements done by listeners of this callback <b>do not</b> affect child models (that is, models whose
+		 * parent hierarchy contains the replaced model), unlike {@link #modifyModelOnLoad}.
+		 */
+		Event<ModelModifier.BeforeBake> modifyModelBeforeBake();
+
+		/**
+		 * Event access to replace the baked model.
+		 */
+		Event<ModelModifier.AfterBake> modifyModelAfterBake();
+
 		/**
 		 * Event access to monitor unbaked block model loads and replace the loaded model.
 		 */
 		Event<ModelModifier.OnLoadBlock> modifyBlockModelOnLoad();
+
+		/**
+		 * Event access to replace the unbaked block model used for baking.
+		 */
+		Event<ModelModifier.BeforeBakeBlock> modifyBlockModelBeforeBake();
+
+		/**
+		 * Event access to replace the baked block model.
+		 */
+		Event<ModelModifier.AfterBakeBlock> modifyBlockModelAfterBake();
 	}
 }
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
index fdfc4f018e..95ad0a4645 100644
--- a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
@@ -20,7 +20,10 @@
 import org.jetbrains.annotations.Nullable;
 
 import net.minecraft.block.BlockState;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
 import net.minecraft.client.render.model.GroupableModel;
+import net.minecraft.client.render.model.ModelBakeSettings;
 import net.minecraft.client.render.model.ResolvableModel;
 import net.minecraft.client.render.model.UnbakedModel;
 import net.minecraft.client.util.ModelIdentifier;
@@ -44,7 +47,7 @@
  * and separate phases are provided for mods that wrap their own models and mods that need to wrap models of other mods
  * or wrap models arbitrarily.
  *
- * <p>These callbacks are invoked for <b>every single model that is loaded</b>, so implementations should be
+ * <p>These callbacks are invoked for <b>every single model that is loaded or baked</b>, so implementations should be
  * as efficient as possible.
  */
 public final class ModelModifier {
@@ -75,7 +78,11 @@ public interface OnLoad {
 		 * <p>If the given model is {@code null}, its corresponding identifier was requested during
 		 * {@linkplain ResolvableModel#resolve resolution} but the model was not loaded normally; i.e. through a JSON
 		 * file, possibly because that file did not exist. If a non-{@code null} model is returned in this case,
-		 * resolution will continue without warnings or errors.
+		 * resolution will continue without warnings or errors. This callback can return a {@code null} model, which
+		 * has the same meaning as described earlier, so it is unlikely that an implementor should need to return
+		 * {@code null} unless directly returning the given model.
+		 *
+		 * <p>For further information, see the docs of {@link ModelLoadingPlugin.Context#modifyModelOnLoad()}.
 		 *
 		 * @param model the current unbaked model instance
 		 * @param context context with additional information about the model/loader
@@ -97,6 +104,86 @@ interface Context {
 		}
 	}
 
+	@FunctionalInterface
+	public interface BeforeBake {
+		/**
+		 * This handler is invoked to allow modification of the unbaked model instance right before it is baked.
+		 *
+		 * <p>For further information, see the docs of {@link ModelLoadingPlugin.Context#modifyModelBeforeBake()}.
+		 *
+		 * @param model the current unbaked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyModelBeforeBake
+		 */
+		UnbakedModel modifyModelBeforeBake(UnbakedModel model, Context context);
+
+		/**
+		 * The context for a before bake model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of the model being baked.
+			 */
+			Identifier id();
+
+			/**
+			 * The settings this model is being baked with.
+			 */
+			ModelBakeSettings settings();
+
+			/**
+			 * The baker being used to bake this model. It can be used to {@linkplain Baker#bake bake models} and
+			 * {@linkplain Baker#getSpriteGetter get sprites}. Note that baking a model which was not previously
+			 * {@linkplain ResolvableModel.Resolver#resolve resolved} will log a warning and return the missing model.
+			 */
+			Baker baker();
+		}
+	}
+
+	@FunctionalInterface
+	public interface AfterBake {
+		/**
+		 * This handler is invoked to allow modification of the baked model instance right after it is baked and before
+		 * it is cached.
+		 *
+		 * @param model the current baked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyModelAfterBake
+		 */
+		BakedModel modifyModelAfterBake(BakedModel model, Context context);
+
+		/**
+		 * The context for an after bake model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of the model being baked.
+			 */
+			Identifier id();
+
+			/**
+			 * The unbaked model that is being baked.
+			 */
+			UnbakedModel sourceModel();
+
+			/**
+			 * The settings this model is being baked with.
+			 */
+			ModelBakeSettings settings();
+
+			/**
+			 * The baker being used to bake this model. It can be used to {@linkplain Baker#bake bake models} and
+			 * {@linkplain Baker#getSpriteGetter get sprites}. Note that baking a model which was not previously
+			 * {@linkplain ResolvableModel.Resolver#resolve resolved} will log a warning and return the missing model.
+			 */
+			Baker baker();
+		}
+	}
+
 	@FunctionalInterface
 	public interface OnLoadBlock {
 		/**
@@ -126,5 +213,72 @@ interface Context {
 		}
 	}
 
+	@FunctionalInterface
+	public interface BeforeBakeBlock {
+		/**
+		 * This handler is invoked to allow modification of the unbaked block model instance right before it is baked.
+		 *
+		 * @param model the current unbaked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyBlockModelBeforeBake
+		 */
+		GroupableModel modifyModelBeforeBake(GroupableModel model, Context context);
+
+		/**
+		 * The context for a before bake block model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of the model being baked.
+			 */
+			ModelIdentifier id();
+
+			/**
+			 * The baker being used to bake this model. It can be used to {@linkplain Baker#bake bake models} and
+			 * {@linkplain Baker#getSpriteGetter get sprites}. Note that baking a model which was not previously
+			 * {@linkplain ResolvableModel.Resolver#resolve resolved} will log a warning and return the missing model.
+			 */
+			Baker baker();
+		}
+	}
+
+	@FunctionalInterface
+	public interface AfterBakeBlock {
+		/**
+		 * This handler is invoked to allow modification of the baked block model instance right after it is baked.
+		 *
+		 * @param model the current baked model instance
+		 * @param context context with additional information about the model/loader
+		 * @return the model that should be used in this scenario. If no changes are needed, just return {@code model} as-is.
+		 * @see ModelLoadingPlugin.Context#modifyBlockModelAfterBake
+		 */
+		BakedModel modifyModelAfterBake(BakedModel model, Context context);
+
+		/**
+		 * The context for an after bake block model modification event.
+		 */
+		@ApiStatus.NonExtendable
+		interface Context {
+			/**
+			 * The identifier of the model being baked.
+			 */
+			ModelIdentifier id();
+
+			/**
+			 * The unbaked model that is being baked.
+			 */
+			GroupableModel sourceModel();
+
+			/**
+			 * The baker being used to bake this model. It can be used to {@linkplain Baker#bake bake models} and
+			 * {@linkplain Baker#getSpriteGetter get sprites}. Note that baking a model which was not previously
+			 * {@linkplain ResolvableModel.Resolver#resolve resolved} will log a warning and return the missing model.
+			 */
+			Baker baker();
+		}
+	}
+
 	private ModelModifier() { }
 }
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelBakerHooks.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelBakerHooks.java
new file mode 100644
index 0000000000..c6a245e6eb
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelBakerHooks.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.impl.client.model.loading;
+
+import org.jetbrains.annotations.Nullable;
+
+public interface ModelBakerHooks {
+	@Nullable
+	ModelLoadingEventDispatcher fabric_getDispatcher();
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
index f6a75e21dc..ad9be411c4 100644
--- a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
@@ -25,6 +25,7 @@
 import java.util.function.Consumer;
 
 import com.google.common.collect.ImmutableList;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
 import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap;
 import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap;
 import org.jetbrains.annotations.Nullable;
@@ -34,8 +35,11 @@
 import net.minecraft.block.Block;
 import net.minecraft.block.BlockState;
 import net.minecraft.client.render.block.BlockModels;
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
 import net.minecraft.client.render.model.BlockStatesLoader;
 import net.minecraft.client.render.model.GroupableModel;
+import net.minecraft.client.render.model.ModelBakeSettings;
 import net.minecraft.client.render.model.UnbakedModel;
 import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.registry.Registries;
@@ -55,7 +59,12 @@ public class ModelLoadingEventDispatcher {
 	private final BlockStateResolverContext blockStateResolverContext = new BlockStateResolverContext();
 
 	private final OnLoadModifierContext onLoadModifierContext = new OnLoadModifierContext();
+	private final ObjectArrayList<BeforeBakeModifierContext> beforeBakeModifierContextStack = new ObjectArrayList<>();
+	private final ObjectArrayList<AfterBakeModifierContext> afterBakeModifierContextStack = new ObjectArrayList<>();
+
 	private final OnLoadBlockModifierContext onLoadBlockModifierContext = new OnLoadBlockModifierContext();
+	private final BeforeBakeBlockModifierContext beforeBakeBlockModifierContext = new BeforeBakeBlockModifierContext();
+	private final AfterBakeBlockModifierContext afterBakeBlockModifierContext = new AfterBakeBlockModifierContext();
 
 	public ModelLoadingEventDispatcher(List<ModelLoadingPlugin> plugins) {
 		this.pluginContext = new ModelLoadingPluginContextImpl();
@@ -79,6 +88,34 @@ public UnbakedModel modifyModelOnLoad(@Nullable UnbakedModel model, Identifier i
 		return pluginContext.modifyModelOnLoad().invoker().modifyModelOnLoad(model, onLoadModifierContext);
 	}
 
+	public UnbakedModel modifyModelBeforeBake(UnbakedModel model, Identifier id, ModelBakeSettings settings, Baker baker) {
+		if (beforeBakeModifierContextStack.isEmpty()) {
+			beforeBakeModifierContextStack.add(new BeforeBakeModifierContext());
+		}
+
+		BeforeBakeModifierContext context = beforeBakeModifierContextStack.pop();
+		context.prepare(id, settings, baker);
+
+		model = pluginContext.modifyModelBeforeBake().invoker().modifyModelBeforeBake(model, context);
+
+		beforeBakeModifierContextStack.push(context);
+		return model;
+	}
+
+	public BakedModel modifyModelAfterBake(BakedModel model, Identifier id, UnbakedModel sourceModel, ModelBakeSettings settings, Baker baker) {
+		if (afterBakeModifierContextStack.isEmpty()) {
+			afterBakeModifierContextStack.add(new AfterBakeModifierContext());
+		}
+
+		AfterBakeModifierContext context = afterBakeModifierContextStack.pop();
+		context.prepare(id, sourceModel, settings, baker);
+
+		model = pluginContext.modifyModelAfterBake().invoker().modifyModelAfterBake(model, context);
+
+		afterBakeModifierContextStack.push(context);
+		return model;
+	}
+
 	public BlockStatesLoader.BlockStateDefinition modifyBlockModelsOnLoad(BlockStatesLoader.BlockStateDefinition models) {
 		Map<ModelIdentifier, BlockStatesLoader.BlockModel> map = models.models();
 
@@ -162,6 +199,16 @@ private GroupableModel modifyBlockModelOnLoad(GroupableModel model, ModelIdentif
 		return pluginContext.modifyBlockModelOnLoad().invoker().modifyModelOnLoad(model, onLoadBlockModifierContext);
 	}
 
+	public GroupableModel modifyBlockModelBeforeBake(GroupableModel model, ModelIdentifier id, Baker baker) {
+		beforeBakeBlockModifierContext.prepare(id, baker);
+		return pluginContext.modifyBlockModelBeforeBake().invoker().modifyModelBeforeBake(model, beforeBakeBlockModifierContext);
+	}
+
+	public BakedModel modifyBlockModelAfterBake(BakedModel model, ModelIdentifier id, GroupableModel sourceModel, Baker baker) {
+		afterBakeBlockModifierContext.prepare(id, sourceModel, baker);
+		return pluginContext.modifyBlockModelAfterBake().invoker().modifyModelAfterBake(model, afterBakeBlockModifierContext);
+	}
+
 	private static class BlockStateResolverContext implements BlockStateResolver.Context {
 		private Block block;
 		private final Reference2ReferenceMap<BlockState, GroupableModel> models = new Reference2ReferenceOpenHashMap<>();
@@ -178,7 +225,7 @@ public Block block() {
 
 		@Override
 		public void setModel(BlockState state, GroupableModel model) {
-			Objects.requireNonNull(model, "state cannot be null");
+			Objects.requireNonNull(state, "state cannot be null");
 			Objects.requireNonNull(model, "model cannot be null");
 
 			if (!state.isOf(block)) {
@@ -204,6 +251,67 @@ public Identifier id() {
 		}
 	}
 
+	private static class BeforeBakeModifierContext implements ModelModifier.BeforeBake.Context {
+		private Identifier id;
+		private ModelBakeSettings settings;
+		private Baker baker;
+
+		private void prepare(Identifier id, ModelBakeSettings settings, Baker baker) {
+			this.id = id;
+			this.settings = settings;
+			this.baker = baker;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public ModelBakeSettings settings() {
+			return settings;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+	}
+
+	private static class AfterBakeModifierContext implements ModelModifier.AfterBake.Context {
+		private Identifier id;
+		private UnbakedModel sourceModel;
+		private ModelBakeSettings settings;
+		private Baker baker;
+
+		private void prepare(Identifier id, UnbakedModel sourceModel, ModelBakeSettings settings, Baker baker) {
+			this.id = id;
+			this.sourceModel = sourceModel;
+			this.settings = settings;
+			this.baker = baker;
+		}
+
+		@Override
+		public Identifier id() {
+			return id;
+		}
+
+		@Override
+		public UnbakedModel sourceModel() {
+			return sourceModel;
+		}
+
+		@Override
+		public ModelBakeSettings settings() {
+			return settings;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+	}
+
 	private static class OnLoadBlockModifierContext implements ModelModifier.OnLoadBlock.Context {
 		private ModelIdentifier id;
 		private BlockState state;
@@ -223,4 +331,51 @@ public BlockState state() {
 			return state;
 		}
 	}
+
+	private static class BeforeBakeBlockModifierContext implements ModelModifier.BeforeBakeBlock.Context {
+		private ModelIdentifier id;
+		private Baker baker;
+
+		private void prepare(ModelIdentifier id, Baker baker) {
+			this.id = id;
+			this.baker = baker;
+		}
+
+		@Override
+		public ModelIdentifier id() {
+			return id;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+	}
+
+	private static class AfterBakeBlockModifierContext implements ModelModifier.AfterBakeBlock.Context {
+		private ModelIdentifier id;
+		private GroupableModel sourceModel;
+		private Baker baker;
+
+		private void prepare(ModelIdentifier id, GroupableModel sourceModel, Baker baker) {
+			this.id = id;
+			this.sourceModel = sourceModel;
+			this.baker = baker;
+		}
+
+		@Override
+		public ModelIdentifier id() {
+			return id;
+		}
+
+		@Override
+		public GroupableModel sourceModel() {
+			return sourceModel;
+		}
+
+		@Override
+		public Baker baker() {
+			return baker;
+		}
+	}
 }
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginContextImpl.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginContextImpl.java
index 41c2d8ad7a..c0a605f888 100644
--- a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginContextImpl.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginContextImpl.java
@@ -57,6 +57,28 @@ public class ModelLoadingPluginContextImpl implements ModelLoadingPlugin.Context
 
 		return model;
 	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.BeforeBake> beforeBakeModifiers = EventFactory.createWithPhases(ModelModifier.BeforeBake.class, modifiers -> (model, context) -> {
+		for (ModelModifier.BeforeBake modifier : modifiers) {
+			try {
+				model = modifier.modifyModelBeforeBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify unbaked model before bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.AfterBake> afterBakeModifiers = EventFactory.createWithPhases(ModelModifier.AfterBake.class, modifiers -> (model, context) -> {
+		for (ModelModifier.AfterBake modifier : modifiers) {
+			try {
+				model = modifier.modifyModelAfterBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify baked model after bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
 	private final Event<ModelModifier.OnLoadBlock> onLoadBlockModifiers = EventFactory.createWithPhases(ModelModifier.OnLoadBlock.class, modifiers -> (model, context) -> {
 		for (ModelModifier.OnLoadBlock modifier : modifiers) {
 			try {
@@ -68,6 +90,28 @@ public class ModelLoadingPluginContextImpl implements ModelLoadingPlugin.Context
 
 		return model;
 	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.BeforeBakeBlock> beforeBakeBlockModifiers = EventFactory.createWithPhases(ModelModifier.BeforeBakeBlock.class, modifiers -> (model, context) -> {
+		for (ModelModifier.BeforeBakeBlock modifier : modifiers) {
+			try {
+				model = modifier.modifyModelBeforeBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify unbaked block model before bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
+	private final Event<ModelModifier.AfterBakeBlock> afterBakeBlockModifiers = EventFactory.createWithPhases(ModelModifier.AfterBakeBlock.class, modifiers -> (model, context) -> {
+		for (ModelModifier.AfterBakeBlock modifier : modifiers) {
+			try {
+				model = modifier.modifyModelAfterBake(model, context);
+			} catch (Exception exception) {
+				LOGGER.error("Failed to modify baked block model after bake", exception);
+			}
+		}
+
+		return model;
+	}, MODEL_MODIFIER_PHASES);
 
 	@Override
 	public void addModels(Identifier... ids) {
@@ -102,8 +146,28 @@ public Event<ModelModifier.OnLoad> modifyModelOnLoad() {
 		return onLoadModifiers;
 	}
 
+	@Override
+	public Event<ModelModifier.BeforeBake> modifyModelBeforeBake() {
+		return beforeBakeModifiers;
+	}
+
+	@Override
+	public Event<ModelModifier.AfterBake> modifyModelAfterBake() {
+		return afterBakeModifiers;
+	}
+
 	@Override
 	public Event<ModelModifier.OnLoadBlock> modifyBlockModelOnLoad() {
 		return onLoadBlockModifiers;
 	}
+
+	@Override
+	public Event<ModelModifier.BeforeBakeBlock> modifyBlockModelBeforeBake() {
+		return beforeBakeBlockModifiers;
+	}
+
+	@Override
+	public Event<ModelModifier.AfterBakeBlock> modifyBlockModelAfterBake() {
+		return afterBakeBlockModifiers;
+	}
 }
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerBakerImplMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerBakerImplMixin.java
new file mode 100644
index 0000000000..fb3b6629f4
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerBakerImplMixin.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2016, 2017, 2018, 2019 FabricMC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.fabricmc.fabric.mixin.client.model.loading;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Coerce;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBakeSettings;
+import net.minecraft.client.render.model.ModelBaker;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.impl.client.model.loading.ModelBakerHooks;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
+
+@Mixin(ModelBaker.BakerImpl.class)
+abstract class ModelBakerBakerImplMixin {
+	@Shadow
+	@Final
+	private ModelBaker field_40571;
+
+	@WrapOperation(method = "bake(Lnet/minecraft/util/Identifier;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;", at = @At(value = "INVOKE", target = "net/minecraft/client/render/model/UnbakedModel.bake(Lnet/minecraft/client/render/model/UnbakedModel;Lnet/minecraft/client/render/model/Baker;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;"))
+	private BakedModel wrapModelBake(UnbakedModel unbakedModel, @Coerce Baker baker, ModelBakeSettings settings, Operation<BakedModel> operation, Identifier id) {
+		ModelLoadingEventDispatcher dispatcher = ((ModelBakerHooks) this.field_40571).fabric_getDispatcher();
+
+		if (dispatcher == null) {
+			return operation.call(unbakedModel, baker, settings);
+		}
+
+		unbakedModel = dispatcher.modifyModelBeforeBake(unbakedModel, id, settings, baker);
+		BakedModel model = operation.call(unbakedModel, baker, settings);
+		return dispatcher.modifyModelAfterBake(model, id, unbakedModel, settings, baker);
+	}
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerMixin.java
index e89d086d3c..cd0e04a491 100644
--- a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerMixin.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/ModelBakerMixin.java
@@ -19,6 +19,8 @@
 import java.util.HashMap;
 import java.util.Map;
 
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
 import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.spongepowered.asm.mixin.Final;
@@ -31,15 +33,19 @@
 import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
 
 import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.GroupableModel;
 import net.minecraft.client.render.model.ModelBaker;
 import net.minecraft.client.render.model.ModelRotation;
+import net.minecraft.client.util.ModelIdentifier;
 import net.minecraft.util.Identifier;
 
 import net.fabricmc.fabric.impl.client.model.loading.BakedModelsHooks;
+import net.fabricmc.fabric.impl.client.model.loading.ModelBakerHooks;
 import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
 
 @Mixin(ModelBaker.class)
-abstract class ModelBakerMixin {
+abstract class ModelBakerMixin implements ModelBakerHooks {
 	@Shadow
 	@Final
 	static Logger LOGGER;
@@ -53,6 +59,17 @@ private void onReturnInit(CallbackInfo ci) {
 		fabric_eventDispatcher = ModelLoadingEventDispatcher.CURRENT.get();
 	}
 
+	@WrapOperation(method = "method_65737", at = @At(value = "INVOKE", target = "net/minecraft/client/render/model/GroupableModel.bake(Lnet/minecraft/client/render/model/Baker;)Lnet/minecraft/client/render/model/BakedModel;"))
+	private BakedModel wrapBlockModelBake(GroupableModel unbakedModel, Baker baker, Operation<BakedModel> operation, ModelBaker.ErrorCollectingSpriteGetter spriteGetter, Map<ModelIdentifier, BakedModel> map, ModelIdentifier id) {
+		if (fabric_eventDispatcher == null) {
+			return operation.call(unbakedModel, baker);
+		}
+
+		unbakedModel = fabric_eventDispatcher.modifyBlockModelBeforeBake(unbakedModel, id, baker);
+		BakedModel model = operation.call(unbakedModel, baker);
+		return fabric_eventDispatcher.modifyBlockModelAfterBake(model, id, unbakedModel, baker);
+	}
+
 	@Inject(method = "bake", at = @At("RETURN"))
 	private void onReturnBake(ModelBaker.ErrorCollectingSpriteGetter spriteGetter, CallbackInfoReturnable<ModelBaker.BakedModels> cir) {
 		if (fabric_eventDispatcher == null) {
@@ -71,4 +88,10 @@ private void onReturnBake(ModelBaker.ErrorCollectingSpriteGetter spriteGetter, C
 		});
 		((BakedModelsHooks) (Object) models).fabric_setExtraModels(extraModels);
 	}
+
+	@Override
+	@Nullable
+	public ModelLoadingEventDispatcher fabric_getDispatcher() {
+		return fabric_eventDispatcher;
+	}
 }
diff --git a/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json b/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json
index f299bdb408..d6e3c40f08 100644
--- a/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json
+++ b/fabric-model-loading-api-v1/src/client/resources/fabric-model-loading-api-v1.mixins.json
@@ -5,6 +5,7 @@
   "client": [
     "BakedModelManagerMixin",
     "ModelBakerBakedModelsMixin",
+    "ModelBakerBakerImplMixin",
     "ModelBakerMixin",
     "ReferencedModelsCollectorMixin",
     "WrapperBakedModelMixin"