From fa7108e6e848fa07b7d5f6ab1e2ec64cde42fa0f Mon Sep 17 00:00:00 2001
From: Adrian Siekierka <kontakt@asie.pl>
Date: Fri, 28 Dec 2018 22:58:19 +0100
Subject: [PATCH] Model loading hooks (#39)

---
 build.gradle                                  |   4 +-
 .../api/client/model/ModelAppender.java       |  27 +++
 .../client/model/ModelLoadingRegistry.java    |  47 +++++
 .../client/model/ModelProviderContext.java    |  36 ++++
 .../client/model/ModelProviderException.java  |  27 +++
 .../client/model/ModelResourceProvider.java   |  52 +++++
 .../client/model/ModelVariantProvider.java    |  49 +++++
 .../impl/client/model/ModelLoaderHooks.java   |  26 +++
 .../model/ModelLoadingRegistryImpl.java       | 188 ++++++++++++++++++
 .../mixin/client/model/MixinModelLoader.java  | 106 ++++++++++
 src/main/resources/fabric.mod.json            |   2 +-
 .../net.fabricmc.fabric.mixins.client.json    |   1 +
 .../fabricmc/fabric/model/ModelModClient.java | 126 ++++++++++++
 13 files changed, 688 insertions(+), 3 deletions(-)
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
 create mode 100644 src/main/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
 create mode 100644 src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
 create mode 100644 src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
 create mode 100644 src/main/java/net/fabricmc/fabric/mixin/client/model/MixinModelLoader.java
 create mode 100644 src/test/java/net/fabricmc/fabric/model/ModelModClient.java

diff --git a/build.gradle b/build.gradle
index 2515605db..2839ee5f0 100644
--- a/build.gradle
+++ b/build.gradle
@@ -26,7 +26,7 @@ targetCompatibility = 1.8
 
 archivesBaseName = "fabric"
 
-def baseVersion = "0.1.2"
+def baseVersion = "0.1.3"
 def mcVersion = "18w50a"
 
 def ENV = System.getenv()
@@ -38,7 +38,7 @@ minecraft {
 
 dependencies {
 	minecraft "com.mojang:minecraft:$mcVersion"
-	mappings "net.fabricmc:yarn:$mcVersion.59"
+	mappings "net.fabricmc:yarn:$mcVersion.64"
 	modCompile "net.fabricmc:fabric-loader:0.3.0.74"
 }
 
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelAppender.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
new file mode 100644
index 000000000..224e4aa42
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+
+import java.util.function.Consumer;
+
+@FunctionalInterface
+public interface ModelAppender {
+	void appendAll(ResourceManager manager, Consumer<ModelIdentifier> out);
+}
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
new file mode 100644
index 000000000..81f287928
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+import net.fabricmc.fabric.impl.client.model.ModelLoadingRegistryImpl;
+import net.minecraft.resource.ResourceManager;
+
+import java.util.function.Function;
+
+public interface ModelLoadingRegistry {
+	ModelLoadingRegistry INSTANCE = ModelLoadingRegistryImpl.INSTANCE;
+
+	/**
+	 * Register a model appender, which can request loading additional models.
+	 *
+	 * @param appender
+	 */
+	void registerAppender(ModelAppender appender);
+
+	/**
+	 * Register a ModelResourceProvider supplier.
+	 *
+	 * @param providerSupplier The ModelResourceProvider supplier, instantiated with every ModelLoader.
+	 */
+	void registerResourceProvider(Function<ResourceManager, ModelResourceProvider> providerSupplier);
+
+	/**
+	 * Register a ModelVariantProvider supplier.
+	 *
+	 * @param providerSupplier The ModelVariantProvider supplier, instantiated with every ModelLoader.
+	 */
+	void registerVariantProvider(Function<ResourceManager, ModelVariantProvider> providerSupplier);
+}
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
new file mode 100644
index 000000000..7e9e79f81
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * The model loading context used during model providing.
+ */
+public interface ModelProviderContext {
+	/**
+	 * Load a model using a {@link Identifier}, {@link ModelIdentifier}, ...
+	 *
+	 * Please note that the game engine keeps track of circular model loading calls on its own.
+	 *
+	 * @param id The model identifier.
+	 * @return The UnbakedModel. Can return a missing model if it's not present!
+	 */
+	UnbakedModel loadModel(Identifier id);
+}
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
new file mode 100644
index 000000000..b15738614
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+public class ModelProviderException extends Exception {
+	public ModelProviderException(String s) {
+		super(s);
+	}
+
+	public ModelProviderException(String s, Throwable t) {
+		super(s, t);
+	}
+}
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
new file mode 100644
index 000000000..c2c790ce1
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Interface for model resource providers.
+ *
+ * Model resource providers hook the loading of model *files* from the resource tree;
+ * that is, in vanilla, it handles going from "minecraft:block/stone" to a
+ * "assets/minecraft/models/block/stone.json" file.
+ *
+ * This is where you want to add your own custom model formats.
+ *
+ * As providers are instantiated with a new provider, it is safe
+ * (and recommended!) to cache information inside a loader.
+ *
+ * Keep in mind that only *one* ModelResourceProvider may respond to a given model
+ * at any time. If you're writing, say, an OBJ loader, this means you could
+ * easily conflict with another OBJ loader unless you take some precautions,
+ * for example:
+ *
+ * a) Only load files with a mod-suffixed name, such as .architect.obj,
+ * b) Only load files from an explicit list of namespaces, registered elsewhere.
+ */
+@FunctionalInterface
+public interface ModelResourceProvider {
+
+	/**
+	 * @param resourceId The resource identifier to be loaded.
+	 * @return The loaded UnbakedModel, or null if this ModelResourceProvider doesn't handle a specific Identifier
+	 * (or if there was no error!).
+	 */
+	/* @Nullable */ UnbakedModel loadModelResource(Identifier resourceId, ModelProviderContext context) throws ModelProviderException;
+}
diff --git a/src/main/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java b/src/main/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
new file mode 100644
index 000000000..eac625d9a
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.api.client.model;
+
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Interface for model variant providers.
+ *
+ * Model variant providers hook the resolution of ModelIdentifiers. In vanilla, this is
+ * the part where a "minecraft:stone#normal" identifier triggers the loading of a
+ * "minecraft:models/stone" model ({@link ModelResourceProvider} handles the later step).
+ *
+ * The most common use of this is to cooperate with a {@link ModelAppender}, but it can
+ * also allow you to add your own block- or item-state formats. To trigger the loading
+ * of another model, use the passed {@link ModelProviderContext}.
+ *
+ * As every model loading is instantiated with a new provider, it is safe
+ * (and recommended!) to cache information.
+ *
+ * Keep in mind that only *one* ModelVariantProvider may respond to a given model
+ * at any time.
+ */
+@FunctionalInterface
+public interface ModelVariantProvider {
+
+	/**
+	 * @param modelId The model identifier, complete with variant.
+	 * @return The loaded UnbakedModel, or null if this ModelVariantProvider doesn't handle a specific Identifier
+	 * (or if there was no error!).
+	 */
+	/* @Nullable */ UnbakedModel loadModelVariant(ModelIdentifier modelId, ModelProviderContext context) throws ModelProviderException;
+}
diff --git a/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java b/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
new file mode 100644
index 000000000..2c07c8364
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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;
+
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+public interface ModelLoaderHooks {
+	public void fabric_addModel(ModelIdentifier id);
+	public UnbakedModel fabric_loadModel(Identifier id);
+}
diff --git a/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java b/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
new file mode 100644
index 000000000..287d05e6a
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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;
+
+import com.google.common.collect.Lists;
+import com.sun.istack.internal.Nullable;
+import net.fabricmc.fabric.api.client.model.*;
+import net.fabricmc.loader.launch.common.FabricLauncherBase;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class ModelLoadingRegistryImpl implements ModelLoadingRegistry {
+	private static final boolean DEBUG_MODEL_LOADING = FabricLauncherBase.getLauncher().isDevelopment()
+		|| Boolean.valueOf(System.getProperty("fabric.debugModelLoading", "false"));
+
+	@FunctionalInterface
+	private static interface CustomModelItf<T> {
+		UnbakedModel load(T obj) throws ModelProviderException;
+	}
+
+	public static class LoaderInstance implements ModelProviderContext {
+		private final Logger logger;
+		private final ResourceManager manager;
+		private final List<ModelVariantProvider> modelVariantProviders;
+		private final List<ModelResourceProvider> modelResourceProviders;
+		private final List<ModelAppender> modelAppenders;
+		private ModelLoader loader;
+
+		private LoaderInstance(ModelLoadingRegistryImpl i, ModelLoader loader, ResourceManager manager) {
+			this.logger = ModelLoadingRegistryImpl.LOGGER;
+			this.loader = loader;
+			this.manager = manager;
+			this.modelVariantProviders = i.variantProviderSuppliers.stream().map((s) -> s.apply(manager)).collect(Collectors.toList());
+			this.modelResourceProviders = i.resourceProviderSuppliers.stream().map((s) -> s.apply(manager)).collect(Collectors.toList());
+			this.modelAppenders = i.appenders;
+		}
+
+		@Override
+		public UnbakedModel loadModel(Identifier id) {
+			if (loader == null) {
+				throw new RuntimeException("Called loadModel too late!");
+			}
+
+			return ((ModelLoaderHooks) loader).fabric_loadModel(id);
+		}
+
+		public void onModelPopulation(Consumer<ModelIdentifier> addModel) {
+			for (ModelAppender appender : modelAppenders) {
+				appender.appendAll(manager, addModel);
+			}
+		}
+
+		private <T> UnbakedModel loadCustomModel(CustomModelItf<T> function, Collection<T> loaders, String debugName) {
+			if (!DEBUG_MODEL_LOADING) {
+				for (T provider : loaders) {
+					try {
+						UnbakedModel model = function.load(provider);
+						if (model != null) {
+							return model;
+						}
+					} catch (ModelProviderException e) {
+						logger.error(e);
+						return null;
+					}
+				}
+
+				return null;
+			}
+
+			UnbakedModel modelLoaded = null;
+			T providerUsed = null;
+			List<T> providersApplied = null;
+
+			for (T provider : loaders) {
+				try {
+					UnbakedModel model = function.load(provider);
+					if (model != null) {
+						if (providersApplied != null) {
+							providersApplied.add(provider);
+						} else if (providerUsed != null) {
+							providersApplied = Lists.newArrayList(providerUsed, provider);
+						} else {
+							modelLoaded = model;
+							providerUsed = provider;
+						}
+					}
+				} catch (ModelProviderException e) {
+					logger.error(e);
+					return null;
+				}
+			}
+
+			if (providersApplied != null) {
+				StringBuilder builder = new StringBuilder("Conflict - multiple " + debugName + "s claimed the same unbaked model:");
+				for (T loader : providersApplied) {
+					builder.append("\n\t - ").append(loader.getClass().getName());
+				}
+				logger.error(builder.toString());
+				return null;
+			} else {
+				return modelLoaded;
+			}
+		}
+
+		@Nullable
+		public UnbakedModel loadModelFromResource(Identifier resourceId) {
+			return loadCustomModel((r) -> r.loadModelResource(resourceId, this), modelResourceProviders, "resource provider");
+		}
+
+		@Nullable
+		public UnbakedModel loadModelFromVariant(Identifier variantId) {
+			if (!(variantId instanceof ModelIdentifier)) {
+				return loadModelFromResource(variantId);
+			} else {
+				ModelIdentifier modelId = (ModelIdentifier) variantId;
+				UnbakedModel model = loadCustomModel((r) -> r.loadModelVariant((ModelIdentifier) variantId, this), modelVariantProviders, "resource provider");
+				if (model != null) {
+					return model;
+				}
+
+				// Replicating the special-case from ModelLoader as loadModelFromJson is insufficiently patchable
+				if (Objects.equals(modelId.getVariant(), "inventory")) {
+					Identifier resourceId = new Identifier(modelId.getNamespace(), "item/" + modelId.getPath());
+					model = loadModelFromResource(resourceId);
+					if (model != null) {
+						return model;
+					}
+				}
+
+				return null;
+			}
+		}
+
+		public void finish() {
+			loader = null;
+		}
+	}
+
+	private static final Logger LOGGER = LogManager.getLogger();
+	public static final ModelLoadingRegistryImpl INSTANCE = new ModelLoadingRegistryImpl();
+
+	private final List<Function<ResourceManager, ModelVariantProvider>> variantProviderSuppliers = new ArrayList<>();
+	private final List<Function<ResourceManager, ModelResourceProvider>> resourceProviderSuppliers = new ArrayList<>();
+	private final List<ModelAppender> appenders = new ArrayList<>();
+
+	@Override
+	public void registerAppender(ModelAppender appender) {
+		appenders.add(appender);
+	}
+
+	@Override
+	public void registerResourceProvider(Function<ResourceManager, ModelResourceProvider> providerSupplier) {
+		resourceProviderSuppliers.add(providerSupplier);
+	}
+
+	@Override
+	public void registerVariantProvider(Function<ResourceManager, ModelVariantProvider> providerSupplier) {
+		variantProviderSuppliers.add(providerSupplier);
+	}
+
+	public static LoaderInstance begin(ModelLoader loader, ResourceManager manager) {
+		return new LoaderInstance(INSTANCE, loader, manager);
+	}
+}
diff --git a/src/main/java/net/fabricmc/fabric/mixin/client/model/MixinModelLoader.java b/src/main/java/net/fabricmc/fabric/mixin/client/model/MixinModelLoader.java
new file mode 100644
index 000000000..408d26ee0
--- /dev/null
+++ b/src/main/java/net/fabricmc/fabric/mixin/client/model/MixinModelLoader.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import net.fabricmc.fabric.impl.client.model.ModelLoaderHooks;
+import net.fabricmc.fabric.impl.client.model.ModelLoadingRegistryImpl;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+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.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.Map;
+import java.util.Set;
+
+@Mixin(ModelLoader.class)
+public class MixinModelLoader implements ModelLoaderHooks {
+	// this is the first one
+	@Shadow
+	public static ModelIdentifier MISSING;
+	@Shadow
+	private ResourceManager resourceContainer;
+	@Shadow
+	private Set<Identifier> field_5390;
+	@Shadow
+	private Map<Identifier, UnbakedModel> unbakedModels;
+
+	private ModelLoadingRegistryImpl.LoaderInstance fabric_mlrLoaderInstance;
+
+	@Shadow
+	private void addModel(ModelIdentifier id) {
+
+	}
+
+	@Shadow
+	private void putModel(Identifier id, UnbakedModel unbakedModel) {
+
+	}
+
+	@Shadow
+	private void loadModel(Identifier id) {
+
+	}
+
+	@Inject(at = @At("HEAD"), method = "loadModel", cancellable = true)
+	private void loadModelHook(Identifier id, CallbackInfo ci) {
+		UnbakedModel customModel = fabric_mlrLoaderInstance.loadModelFromVariant(id);
+		if (customModel != null) {
+			putModel(id, customModel);
+			ci.cancel();
+		}
+	}
+
+	@Inject(at = @At("HEAD"), method = "addModel")
+	private void addModelHook(ModelIdentifier id, CallbackInfo info) {
+		if (id == MISSING) {
+			//noinspection RedundantCast
+			ModelLoaderHooks hooks = (ModelLoaderHooks) (Object) this;
+
+			fabric_mlrLoaderInstance = ModelLoadingRegistryImpl.begin((ModelLoader) (Object) this, resourceContainer);
+			fabric_mlrLoaderInstance.onModelPopulation(hooks::fabric_addModel);
+		}
+	}
+
+	@Inject(at = @At("RETURN"), method = "<init>")
+	private void initFinishedHook(CallbackInfo info) {
+		//noinspection ConstantConditions
+		fabric_mlrLoaderInstance.finish();
+	}
+
+	@Override
+	public void fabric_addModel(ModelIdentifier id) {
+		addModel(id);
+	}
+
+	@Override
+	public UnbakedModel fabric_loadModel(Identifier id) {
+		if (!field_5390.add(id)) {
+			throw new IllegalStateException("Circular reference while loading " + id);
+		}
+		loadModel(id);
+		field_5390.remove(id);
+		return unbakedModels.get(id);
+	}
+}
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index b2ad0f055..345e7e971 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -1,7 +1,7 @@
 {
   "id": "fabric",
   "name": "Fabric API",
-  "version": "0.1.2",
+  "version": "0.1.3",
   "side": "universal",
   "description": "Core API module providing key hooks and intercompatibility features.",
   "license": "Apache-2.0",
diff --git a/src/main/resources/net.fabricmc.fabric.mixins.client.json b/src/main/resources/net.fabricmc.fabric.mixins.client.json
index b4f122565..b95567397 100644
--- a/src/main/resources/net.fabricmc.fabric.mixins.client.json
+++ b/src/main/resources/net.fabricmc.fabric.mixins.client.json
@@ -7,6 +7,7 @@
     "bugfix.MixinBiomeColors",
     "client.itemgroup.MixinItemGroup",
     "client.itemgroup.MixinCreativePlayerInventoryGui",
+    "client.model.MixinModelLoader",
     "client.render.MixinBlockColorMap",
     "client.render.MixinBlockEntityRenderManager",
     "client.render.MixinEntityRenderManager",
diff --git a/src/test/java/net/fabricmc/fabric/model/ModelModClient.java b/src/test/java/net/fabricmc/fabric/model/ModelModClient.java
new file mode 100644
index 000000000..0746b6c05
--- /dev/null
+++ b/src/test/java/net/fabricmc/fabric/model/ModelModClient.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (c) 2016, 2017, 2018 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.model;
+
+import net.fabricmc.api.ClientModInitializer;
+import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
+import net.fabricmc.fabric.events.client.ClientTickEvent;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.render.model.*;
+import net.minecraft.client.render.model.json.ModelItemPropertyOverrideList;
+import net.minecraft.client.render.model.json.ModelTransformation;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.math.Direction;
+
+import java.util.*;
+import java.util.function.Function;
+
+public class ModelModClient implements ClientModInitializer {
+	private static BakedModel bakedModel;
+
+	@Override
+	public void onInitializeClient() {
+		ModelLoadingRegistry.INSTANCE.registerAppender((manager, out) -> {
+			System.out.println("--- ModelAppender called! ---");
+			out.accept(new ModelIdentifier("fabric:model#custom"));
+		});
+
+		ModelLoadingRegistry.INSTANCE.registerVariantProvider(manager -> ((modelId, context) -> {
+			if (modelId.getVariant().equals("custom") && modelId.getNamespace().equals("fabric")) {
+				System.out.println("--- ModelVariantProvider called! ---");
+				return context.loadModel(new Identifier("fabric:custom"));
+			} else {
+				return null;
+			}
+		}));
+
+		ModelLoadingRegistry.INSTANCE.registerResourceProvider(manager -> ((id, context) -> {
+			if (id.toString().equals("fabric:custom")) {
+				return context.loadModel(new Identifier("fabric:custom2"));
+			} else if (id.toString().equals("fabric:custom2")) {
+				System.out.println("--- ModelResourceProvider called! ---");
+				return new UnbakedModel() {
+					@Override
+					public Collection<Identifier> getModelDependencies() {
+						return Collections.emptyList();
+					}
+
+					@Override
+					public Collection<Identifier> getTextureDependencies(Function<Identifier, UnbakedModel> var1, Set<String> var2) {
+						return Collections.emptyList();
+					}
+
+					@Override
+					public BakedModel bake(ModelLoader var1, Function<Identifier, Sprite> var2, ModelRotationContainer var3) {
+						System.out.println("--- Model baked! ---");
+
+						return bakedModel = new BakedModel() {
+							@Override
+							public List<BakedQuad> getQuads(BlockState var1, Direction var2, Random var3) {
+								return Collections.emptyList();
+							}
+
+							@Override
+							public boolean useAmbientOcclusion() {
+								return false;
+							}
+
+							@Override
+							public boolean hasDepthInGui() {
+								return false;
+							}
+
+							@Override
+							public boolean isBuiltin() {
+								return false;
+							}
+
+							@Override
+							public Sprite getSprite() {
+								return MinecraftClient.getInstance().getSpriteAtlas().getSprite("missingno");
+							}
+
+							@Override
+							public ModelTransformation getTransformations() {
+								return ModelTransformation.ORIGIN;
+							}
+
+							@Override
+							public ModelItemPropertyOverrideList getItemPropertyOverrides() {
+								return ModelItemPropertyOverrideList.ORIGIN;
+							}
+						};
+					}
+				};
+			} else {
+				return null;
+			}
+		}));
+
+		ClientTickEvent.CLIENT.register((client) -> {
+			if (client.getBakedModelManager().getModel(new ModelIdentifier("fabric:model#custom"))
+				== bakedModel && bakedModel != null) {
+				System.out.println("--- MODEL LOADED! ---");
+			} else {
+				System.out.println("--- MODEL NOT LOADED! ---");
+			}
+		});
+	}
+}