diff --git a/deprecated/fabric-models-v0/build.gradle b/deprecated/fabric-models-v0/build.gradle
new file mode 100644
index 0000000000..c5841fda4b
--- /dev/null
+++ b/deprecated/fabric-models-v0/build.gradle
@@ -0,0 +1,7 @@
+archivesBaseName = "fabric-models-v0"
+version = getSubprojectVersion(project)
+
+moduleDependencies(project, [
+ 'fabric-api-base',
+ 'fabric-model-loading-api-v1'
+])
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
similarity index 89%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
index 3eea16ad15..d13bec8c80 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/BakedModelManagerHelper.java
@@ -23,8 +23,12 @@
import net.minecraft.client.util.ModelIdentifier;
import net.minecraft.util.Identifier;
-import net.fabricmc.fabric.impl.client.model.BakedModelManagerHooks;
+import net.fabricmc.fabric.api.client.model.loading.v1.FabricBakedModelManager;
+/**
+ * @deprecated Use {@link FabricBakedModelManager#getModel(Identifier)} instead.
+ */
+@Deprecated
public final class BakedModelManagerHelper {
/**
* An alternative to {@link BakedModelManager#getModel(ModelIdentifier)} that accepts an
@@ -42,7 +46,7 @@ public final class BakedModelManagerHelper {
*/
@Nullable
public static BakedModel getModel(BakedModelManager manager, Identifier id) {
- return ((BakedModelManagerHooks) manager).fabric_getModel(id);
+ return manager.getModel(id);
}
private BakedModelManagerHelper() { }
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
similarity index 90%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
index 8293166531..0a6afcbed8 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ExtraModelProvider.java
@@ -22,6 +22,12 @@
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
+/**
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
+ */
+@Deprecated
@FunctionalInterface
public interface ExtraModelProvider {
/**
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
similarity index 100%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelAppender.java
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
index b0f041da0f..8f72a5d046 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelLoadingRegistry.java
@@ -21,8 +21,13 @@
import net.minecraft.resource.ResourceManager;
import net.minecraft.util.Identifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
import net.fabricmc.fabric.impl.client.model.ModelLoadingRegistryImpl;
+/**
+ * @deprecated Register a {@link ModelLoadingPlugin} instead.
+ */
+@Deprecated
public interface ModelLoadingRegistry {
ModelLoadingRegistry INSTANCE = new ModelLoadingRegistryImpl();
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
similarity index 88%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
index 3aafc7014a..96b097d285 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderContext.java
@@ -20,9 +20,13 @@
import net.minecraft.client.util.ModelIdentifier;
import net.minecraft.util.Identifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
/**
* The model loading context used during model providing.
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
*/
+@Deprecated
public interface ModelProviderContext {
/**
* Load a model using a {@link Identifier}, {@link ModelIdentifier}, ...
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
similarity index 83%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
index 1d7899927e..41805578cc 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelProviderException.java
@@ -16,6 +16,12 @@
package net.fabricmc.fabric.api.client.model;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
+/**
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
+ */
+@Deprecated
public class ModelProviderException extends Exception {
public ModelProviderException(String s) {
super(s);
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
index b7e7eb60b0..caf7b1a49a 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelResourceProvider.java
@@ -21,6 +21,8 @@
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.util.Identifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
/**
* Interface for model resource providers.
*
@@ -40,7 +42,10 @@
*
*
- Only load files with a mod-suffixed name, such as .architect.obj,
*
- Only load files from an explicit list of namespaces, registered elsewhere.
+ *
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
*/
+@Deprecated
@FunctionalInterface
public interface ModelResourceProvider {
/**
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
similarity index 92%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
rename to deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
index b2ab8e6c5d..1fdf970b2b 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/api/client/model/ModelVariantProvider.java
@@ -21,6 +21,8 @@
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.util.ModelIdentifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+
/**
* Interface for model variant providers.
*
@@ -37,7 +39,10 @@
*
* Keep in mind that only *one* ModelVariantProvider may respond to a given model
* at any time.
+ *
+ * @deprecated Use {@link ModelLoadingPlugin} and related classes instead.
*/
+@Deprecated
@FunctionalInterface
public interface ModelVariantProvider {
/**
diff --git a/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
new file mode 100644
index 0000000000..6ab871a580
--- /dev/null
+++ b/deprecated/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoadingRegistryImpl.java
@@ -0,0 +1,111 @@
+/*
+ * 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;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.ExtraModelProvider;
+import net.fabricmc.fabric.api.client.model.ModelAppender;
+import net.fabricmc.fabric.api.client.model.ModelLoadingRegistry;
+import net.fabricmc.fabric.api.client.model.ModelProviderContext;
+import net.fabricmc.fabric.api.client.model.ModelProviderException;
+import net.fabricmc.fabric.api.client.model.ModelResourceProvider;
+import net.fabricmc.fabric.api.client.model.ModelVariantProvider;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderPluginContextImpl;
+
+public class ModelLoadingRegistryImpl implements ModelLoadingRegistry {
+ private final List modelProviders = new ArrayList<>();
+ private final List modelAppenders = new ArrayList<>();
+ private final List> resourceProviderSuppliers = new ArrayList<>();
+ private final List> variantProviderSuppliers = new ArrayList<>();
+
+ {
+ // Grabs the resource manager to use it in the main model loading code.
+ // When using the v1 API, data should be loaded in parallel before model loading starts.
+ PreparableModelLoadingPlugin.register(
+ (resourceManager, executor) -> CompletableFuture.completedFuture(resourceManager),
+ this::onInitializeModelLoader);
+ }
+
+ private void onInitializeModelLoader(ResourceManager resourceManager, ModelLoadingPlugin.Context pluginContext) {
+ Consumer extraModelConsumer = pluginContext::addModels;
+ Consumer extraModelConsumer2 = pluginContext::addModels;
+ // A bit hacky, but avoids the allocation of a new context wrapper every time.
+ ModelProviderContext resourceProviderContext = ((ModelLoaderPluginContextImpl) pluginContext).modelGetter::apply;
+
+ for (ExtraModelProvider provider : modelProviders) {
+ provider.provideExtraModels(resourceManager, extraModelConsumer);
+ }
+
+ for (ModelAppender appender : modelAppenders) {
+ appender.appendAll(resourceManager, extraModelConsumer2);
+ }
+
+ for (Function supplier : resourceProviderSuppliers) {
+ ModelResourceProvider provider = supplier.apply(resourceManager);
+
+ pluginContext.resolveModel().register(resolverContext -> {
+ try {
+ return provider.loadModelResource(resolverContext.id(), resourceProviderContext);
+ } catch (ModelProviderException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+
+ for (Function supplier : variantProviderSuppliers) {
+ ModelVariantProvider provider = supplier.apply(resourceManager);
+ ((ModelLoaderPluginContextImpl) pluginContext).legacyVariantProviders().register(modelId -> {
+ try {
+ return provider.loadModelVariant(modelId, resourceProviderContext);
+ } catch (ModelProviderException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void registerModelProvider(ExtraModelProvider provider) {
+ modelProviders.add(provider);
+ }
+
+ @Override
+ public void registerAppender(ModelAppender appender) {
+ modelAppenders.add(appender);
+ }
+
+ @Override
+ public void registerResourceProvider(Function providerSupplier) {
+ resourceProviderSuppliers.add(providerSupplier);
+ }
+
+ @Override
+ public void registerVariantProvider(Function providerSupplier) {
+ variantProviderSuppliers.add(providerSupplier);
+ }
+}
diff --git a/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png b/deprecated/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
similarity index 100%
rename from fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
rename to deprecated/fabric-models-v0/src/client/resources/assets/fabric-models-v0/icon.png
diff --git a/fabric-models-v0/src/client/resources/fabric.mod.json b/deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
similarity index 82%
rename from fabric-models-v0/src/client/resources/fabric.mod.json
rename to deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
index 93a4b284b5..ced3a503e8 100644
--- a/fabric-models-v0/src/client/resources/fabric.mod.json
+++ b/deprecated/fabric-models-v0/src/client/resources/fabric.mod.json
@@ -17,13 +17,11 @@
],
"depends": {
"fabricloader": ">=0.4.0",
- "fabric-api-base": "*"
+ "fabric-api-base": "*",
+ "fabric-model-loading-api-v1": "*"
},
"description": "Hooks for models and model loading.",
- "mixins": [
- "fabric-models-v0.mixins.json"
- ],
"custom": {
- "fabric-api:module-lifecycle": "stable"
+ "fabric-api:module-lifecycle": "deprecated"
}
}
diff --git a/fabric-models-v0/build.gradle b/fabric-model-loading-api-v1/build.gradle
similarity index 69%
rename from fabric-models-v0/build.gradle
rename to fabric-model-loading-api-v1/build.gradle
index 58fefe77b6..7831d25ef6 100644
--- a/fabric-models-v0/build.gradle
+++ b/fabric-model-loading-api-v1/build.gradle
@@ -1,9 +1,10 @@
-archivesBaseName = "fabric-models-v0"
+archivesBaseName = "fabric-model-loading-api-v1"
version = getSubprojectVersion(project)
moduleDependencies(project, ['fabric-api-base'])
testDependencies(project, [
+ ':fabric-renderer-api-v1',
':fabric-rendering-v1',
':fabric-resource-loader-v0'
])
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java
new file mode 100644
index 0000000000..dacd2f1a4f
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/BlockStateResolver.java
@@ -0,0 +1,90 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.block.Block;
+import net.minecraft.block.BlockState;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Block state resolvers are responsible for mapping each {@link BlockState} of a block to an {@link UnbakedModel}.
+ * They replace the {@code blockstates/} JSON files. One block can be mapped to only one block state resolver; multiple
+ * resolvers will not receive the same block.
+ *
+ * Block state resolvers can be used to create custom block state formats or dynamically resolve block state models.
+ *
+ *
Use {@link ModelResolver} instead of this interface if interacting with the block and block states directly is not
+ * necessary. This includes custom model deserializers and loaders.
+ *
+ * @see ModelResolver
+ * @see ModelModifier.OnLoad
+ */
+@FunctionalInterface
+public interface BlockStateResolver {
+ /**
+ * Resolves the models for all block states of the block.
+ *
+ *
For each block state, call {@link Context#setModel} to set its unbaked model.
+ * This method must be called exactly once for each block state.
+ *
+ *
Note that if multiple block states share the same unbaked model instance, it will be baked multiple times
+ * (once per block state that has the model set), which is not efficient. To improve efficiency in this case, the
+ * model should be delegated to using {@link DelegatingUnbakedModel} to ensure that it is only baked once. The inner
+ * model can be loaded using {@link ModelResolver} if custom loading logic is necessary.
+ */
+ void resolveBlockStates(Context context);
+
+ /**
+ * The context for block state resolution.
+ */
+ @ApiStatus.NonExtendable
+ interface Context {
+ /**
+ * The block for which block state models are being resolved.
+ */
+ Block block();
+
+ /**
+ * Sets the model for a block state.
+ *
+ * @param state the block state for which this model should be used
+ * @param model the unbaked model for this block state
+ */
+ void setModel(BlockState state, UnbakedModel model);
+
+ /**
+ * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
+ *
+ * @param id the model identifier
+ * @return the unbaked model, or a missing model if it is not present
+ */
+ UnbakedModel getOrLoadModel(Identifier id);
+
+ /**
+ * The current model loader instance, which changes between resource reloads.
+ *
+ *
Do not call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
+ * use {@link #getOrLoadModel} from the context instead.
+ */
+ ModelLoader loader();
+ }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java
new file mode 100644
index 0000000000..ee37263fd2
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/DelegatingUnbakedModel.java
@@ -0,0 +1,67 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Nullable;
+
+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.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * An unbaked model that returns another {@link BakedModel} at {@linkplain #bake bake time}.
+ * This allows multiple {@link UnbakedModel}s to share the same {@link BakedModel} instance
+ * and prevents baking the same model multiple times.
+ */
+public final class DelegatingUnbakedModel implements UnbakedModel {
+ private final Identifier delegate;
+ private final List dependencies;
+
+ /**
+ * Constructs a new delegating model.
+ *
+ * @param delegate The identifier (can be a {@link ModelIdentifier}) of the underlying baked model.
+ */
+ public DelegatingUnbakedModel(Identifier delegate) {
+ this.delegate = delegate;
+ this.dependencies = List.of(delegate);
+ }
+
+ @Override
+ public Collection getModelDependencies() {
+ return dependencies;
+ }
+
+ @Override
+ public void setParents(Function modelLoader) {
+ }
+
+ @Nullable
+ @Override
+ public BakedModel bake(Baker baker, Function textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
+ return baker.bake(delegate, rotationContainer);
+ }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java
new file mode 100644
index 0000000000..dbc5e0c4c7
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/FabricBakedModelManager.java
@@ -0,0 +1,50 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Fabric-provided helper methods for {@link BakedModelManager}.
+ *
+ * Note: This interface is automatically implemented on the {@link BakedModelManager} via Mixin and interface injection.
+ */
+public interface FabricBakedModelManager {
+ /**
+ * An alternative to {@link BakedModelManager#getModel(ModelIdentifier)} that accepts an
+ * {@link Identifier} instead. Models loaded using {@link ModelLoadingPlugin.Context#addModels}
+ * do not have a corresponding {@link ModelIdentifier}, so the vanilla method cannot be used to
+ * retrieve them. The {@link Identifier} that was used to load them can be used in this method
+ * to retrieve them.
+ *
+ *
This method, as well as its vanilla counterpart, should only be used after the
+ * {@link BakedModelManager} has completed reloading. Otherwise, the result will be
+ * outdated or null.
+ *
+ * @param id the id of the model
+ * @return the model
+ */
+ @Nullable
+ default BakedModel getModel(Identifier id) {
+ throw new UnsupportedOperationException("Implemented via mixin.");
+ }
+}
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
new file mode 100644
index 0000000000..002deb2fae
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelLoadingPlugin.java
@@ -0,0 +1,98 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import java.util.Collection;
+
+import org.jetbrains.annotations.ApiStatus;
+
+import net.minecraft.block.Block;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+/**
+ * A model loading plugin is used to extend the model loading process through the passed {@link Context} object.
+ *
+ *
{@link PreparableModelLoadingPlugin} can be used if some resources need to be loaded from the
+ * {@link ResourceManager}.
+ */
+@FunctionalInterface
+public interface ModelLoadingPlugin {
+ /**
+ * Registers a model loading plugin.
+ */
+ static void register(ModelLoadingPlugin plugin) {
+ ModelLoadingPluginManager.registerPlugin(plugin);
+ }
+
+ /**
+ * Called towards the beginning of the model loading process, every time resource are (re)loaded.
+ * Use the context object to extend model loading as desired.
+ */
+ void onInitializeModelLoader(Context pluginContext);
+
+ @ApiStatus.NonExtendable
+ interface Context {
+ /**
+ * Adds one or more models (can be {@link ModelIdentifier}s) to the list of models that will be loaded and
+ * baked.
+ */
+ void addModels(Identifier... ids);
+
+ /**
+ * Adds multiple models (can be {@link ModelIdentifier}s) to the list of models that will be loaded and baked.
+ */
+ void addModels(Collection extends Identifier> ids);
+
+ /**
+ * Registers a block state resolver for a block.
+ *
+ *
The block must be registered and a block state resolver must not have been previously registered for the
+ * block.
+ */
+ void registerBlockStateResolver(Block block, BlockStateResolver resolver);
+
+ /**
+ * Event access to register model resolvers.
+ */
+ Event resolveModel();
+
+ /**
+ * Event access to monitor unbaked model loads and replace the loaded model.
+ */
+ Event modifyModelOnLoad();
+
+ /**
+ * Event access to replace the unbaked model used for baking without replacing the cached model.
+ *
+ * This is useful for mods which wish to wrap a model without affecting other models that use it as a parent
+ * (e.g. wrap a block's model into a non-{@link JsonUnbakedModel} class but still allow the item model to be
+ * loaded and baked without exceptions).
+ */
+ Event modifyModelBeforeBake();
+
+ /**
+ * Event access to monitor baked model loads and replace the loaded model.
+ */
+ Event modifyModelAfterBake();
+ }
+}
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
new file mode 100644
index 0000000000..8a6fb0894e
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelModifier.java
@@ -0,0 +1,220 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import java.util.function.Function;
+
+import org.jetbrains.annotations.ApiStatus;
+
+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.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.event.Event;
+
+/**
+ * Contains interfaces for the events that can be used to modify models at different points in the loading and baking
+ * process.
+ *
+ * Example use cases:
+ *
+ * - Overriding a model for a particular block state - check if the given identifier is a {@link ModelIdentifier},
+ * and then check if it has the appropriate variant for that block state. If so, return your desired model,
+ * otherwise return the given model.
+ * - Wrapping a model to override certain behaviors - simply return a new model instance and delegate calls
+ * to the original model as needed.
+ *
+ *
+ * Phases are used to ensure that modifications occur in a reasonable order, e.g. wrapping occurs after overrides,
+ * 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.
+ *
+ *
These callbacks are invoked for every single model that is loaded or baked, so implementations should be
+ * as efficient as possible.
+ */
+public final class ModelModifier {
+ /**
+ * Recommended phase to use when overriding models, e.g. replacing a model with another model.
+ */
+ public static final Identifier OVERRIDE_PHASE = new Identifier("fabric", "override");
+ /**
+ * Recommended phase to use for transformations that need to happen before wrapping, but after model overrides.
+ */
+ public static final Identifier DEFAULT_PHASE = Event.DEFAULT_PHASE;
+ /**
+ * Recommended phase to use when wrapping models.
+ */
+ public static final Identifier WRAP_PHASE = new Identifier("fabric", "wrap");
+ /**
+ * Recommended phase to use when wrapping models with transformations that want to happen last,
+ * e.g. for connected textures or other similar visual effects that should be the final processing step.
+ */
+ public static final Identifier WRAP_LAST_PHASE = new Identifier("fabric", "wrap_last");
+
+ @FunctionalInterface
+ public interface OnLoad {
+ /**
+ * This handler is invoked to allow modification of an unbaked model right after it is first loaded and before
+ * it is cached.
+ *
+ * @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#modifyModelOnLoad
+ */
+ UnbakedModel modifyModelOnLoad(UnbakedModel model, Context context);
+
+ /**
+ * The context for an on load model modification event.
+ */
+ @ApiStatus.NonExtendable
+ interface Context {
+ /**
+ * The identifier of this model (may be a {@link ModelIdentifier}).
+ *
+ *
For item models, only the {@link ModelIdentifier} with the {@code inventory} variant is passed, and
+ * not the corresponding plain identifier.
+ */
+ Identifier id();
+
+ /**
+ * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already
+ * loaded.
+ *
+ * @param id the model identifier
+ * @return the unbaked model, or a missing model if it is not present
+ */
+ UnbakedModel getOrLoadModel(Identifier id);
+
+ /**
+ * The current model loader instance, which changes between resource reloads.
+ *
+ *
Do not call {@link ModelLoader#getOrLoadModel} as it does not supported nested model
+ * resolution; use {@link #getOrLoadModel} from the context instead.
+ */
+ ModelLoader loader();
+ }
+ }
+
+ @FunctionalInterface
+ public interface BeforeBake {
+ /**
+ * This handler is invoked to allow modification of the unbaked 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#modifyModelBeforeBake
+ */
+ UnbakedModel modifyModelBeforeBake(UnbakedModel model, Context context);
+
+ /**
+ * The context for a before bake model modification event.
+ */
+ @ApiStatus.NonExtendable
+ interface Context {
+ /**
+ * The identifier of this model (may be a {@link ModelIdentifier}).
+ */
+ Identifier id();
+
+ /**
+ * The function that can be used to retrieve sprites.
+ */
+ Function textureGetter();
+
+ /**
+ * 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#getOrLoadModel load unbaked models} and
+ * {@linkplain Baker#bake load baked models}.
+ */
+ Baker baker();
+
+ /**
+ * The current model loader instance, which changes between resource reloads.
+ */
+ ModelLoader loader();
+ }
+ }
+
+ @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.
+ *
+ * For further information, see the docs of {@link ModelLoadingPlugin.Context#modifyModelAfterBake()}.
+ *
+ * @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 this model (may be a {@link ModelIdentifier}).
+ */
+ Identifier id();
+
+ /**
+ * The unbaked model that is being baked.
+ */
+ UnbakedModel sourceModel();
+
+ /**
+ * The function that can be used to retrieve sprites.
+ */
+ Function textureGetter();
+
+ /**
+ * 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#getOrLoadModel load unbaked models} and
+ * {@linkplain Baker#bake load baked models}.
+ */
+ Baker baker();
+
+ /**
+ * The current model loader instance, which changes between resource reloads.
+ */
+ ModelLoader loader();
+ }
+ }
+
+ private ModelModifier() { }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java
new file mode 100644
index 0000000000..8236af0319
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/ModelResolver.java
@@ -0,0 +1,82 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.util.Identifier;
+
+/**
+ * Model resolvers are able to provide a custom model for specific {@link Identifier}s.
+ * In vanilla, these {@link Identifier}s are converted to file paths and used to load
+ * a model from JSON. Since model resolvers override this process, they can be used to
+ * create custom model formats.
+ *
+ * Only one resolver may provide a custom model for a certain {@link Identifier}.
+ * Thus, resolvers that load models using a custom format could conflict. To avoid
+ * conflicts, such resolvers may want to only load files with a mod-suffixed name
+ * or only load files that have been explicitly defined elsewhere.
+ *
+ *
If it is necessary to load and bake an arbitrary model that is not referenced
+ * normally, a model resolver can be used in conjunction with
+ * {@link ModelLoadingPlugin.Context#addModels} to directly load and bake custom model
+ * instances.
+ *
+ *
Model resolvers are invoked for every single model that will be loaded,
+ * so implementations should be as efficient as possible.
+ *
+ * @see ModelLoadingPlugin.Context#addModels
+ */
+@FunctionalInterface
+public interface ModelResolver {
+ /**
+ * @return the resolved {@link UnbakedModel}, or {@code null} if this resolver does not handle the current {@link Identifier}
+ */
+ @Nullable
+ UnbakedModel resolveModel(Context context);
+
+ /**
+ * The context for model resolution.
+ */
+ @ApiStatus.NonExtendable
+ interface Context {
+ /**
+ * The identifier of the model to be loaded.
+ */
+ Identifier id();
+
+ /**
+ * Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
+ *
+ * @param id the model identifier
+ * @return the unbaked model, or a missing model if it is not present
+ */
+ UnbakedModel getOrLoadModel(Identifier id);
+
+ /**
+ * The current model loader instance, which changes between resource reloads.
+ *
+ *
Do not call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
+ * use {@link #getOrLoadModel} from the context instead.
+ */
+ ModelLoader loader();
+ }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java
new file mode 100644
index 0000000000..b900cffc51
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/api/client/model/loading/v1/PreparableModelLoadingPlugin.java
@@ -0,0 +1,66 @@
+/*
+ * 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.api.client.model.loading.v1;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.function.Supplier;
+
+import net.minecraft.resource.ResourceManager;
+
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+/**
+ * A model loading plugin is used to extend the model loading process through the passed
+ * {@link ModelLoadingPlugin.Context} object.
+ *
+ *
This version of {@link ModelLoadingPlugin} allows loading ("preparing") some data off-thread in parallel before
+ * the model loading process starts. Usually, this means loading some resources from the provided
+ * {@link ResourceManager}.
+ */
+@FunctionalInterface
+public interface PreparableModelLoadingPlugin {
+ /**
+ * Registers a preparable model loading plugin.
+ */
+ static void register(DataLoader loader, PreparableModelLoadingPlugin plugin) {
+ ModelLoadingPluginManager.registerPlugin(loader, plugin);
+ }
+
+ /**
+ * Called towards the beginning of the model loading process, every time resource are (re)loaded.
+ * Use the context object to extend model loading as desired.
+ *
+ * @param data The data loaded by the {@link DataLoader}.
+ * @param pluginContext The context that can be used to extend model loading.
+ */
+ void onInitializeModelLoader(T data, ModelLoadingPlugin.Context pluginContext);
+
+ @FunctionalInterface
+ interface DataLoader {
+ /**
+ * Returns a {@link CompletableFuture} that will load the data.
+ * Do not block the thread when this function is called, rather use
+ * {@link CompletableFuture#supplyAsync(Supplier, Executor)} to compute the data.
+ * The completable future should be scheduled to run using the passed executor.
+ *
+ * @param resourceManager The resource manager that can be used to retrieve resources.
+ * @param executor The executor that must be used to schedule any completable future.
+ */
+ CompletableFuture load(ResourceManager resourceManager, Executor executor);
+ }
+}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
similarity index 71%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
index e93a75b232..3b70bc224c 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/ModelLoaderHooks.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/BlockStateResolverHolder.java
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-package net.fabricmc.fabric.impl.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
-import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.block.Block;
import net.minecraft.util.Identifier;
-public interface ModelLoaderHooks {
- void fabric_addModel(Identifier id);
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
- UnbakedModel fabric_loadModel(Identifier id);
+record BlockStateResolverHolder(BlockStateResolver resolver, Block block, Identifier blockId) {
}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
similarity index 61%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
index b4748ab6f2..ecc5ddc0ba 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/impl/client/model/BakedModelManagerHooks.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/LegacyModelVariantProvider.java
@@ -14,11 +14,17 @@
* limitations under the License.
*/
-package net.fabricmc.fabric.impl.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
-import net.minecraft.client.render.model.BakedModel;
-import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.Nullable;
-public interface BakedModelManagerHooks {
- BakedModel fabric_getModel(Identifier id);
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+
+/**
+ * Legacy v0 bridge - remove if the legacy v0 module is removed.
+ */
+public interface LegacyModelVariantProvider {
+ @Nullable
+ UnbakedModel loadModelVariant(ModelIdentifier modelId);
}
diff --git a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
similarity index 52%
rename from fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java
rename to fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
index 8117abbfe7..67e1dfd264 100644
--- a/fabric-models-v0/src/client/java/net/fabricmc/fabric/mixin/client/model/BakedModelManagerMixin.java
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderHooks.java
@@ -14,26 +14,24 @@
* limitations under the License.
*/
-package net.fabricmc.fabric.mixin.client.model;
+package net.fabricmc.fabric.impl.client.model.loading;
-import java.util.Map;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.util.Identifier;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.Shadow;
+public interface ModelLoaderHooks {
+ ModelLoadingEventDispatcher fabric_getDispatcher();
-import net.minecraft.client.render.model.BakedModel;
-import net.minecraft.client.render.model.BakedModelManager;
-import net.minecraft.util.Identifier;
+ UnbakedModel fabric_getMissingModel();
+
+ UnbakedModel fabric_getOrLoadModel(Identifier id);
+
+ void fabric_putModel(Identifier id, UnbakedModel model);
-import net.fabricmc.fabric.impl.client.model.BakedModelManagerHooks;
+ void fabric_putModelDirectly(Identifier id, UnbakedModel model);
-@Mixin(BakedModelManager.class)
-public class BakedModelManagerMixin implements BakedModelManagerHooks {
- @Shadow
- private Map models;
+ void fabric_queueModelDependencies(UnbakedModel model);
- @Override
- public BakedModel fabric_getModel(Identifier id) {
- return models.get(id);
- }
+ JsonUnbakedModel fabric_loadModelFromJson(Identifier id);
}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java
new file mode 100644
index 0000000000..ba4ceec640
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoaderPluginContextImpl.java
@@ -0,0 +1,223 @@
+/*
+ * 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 java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import net.minecraft.block.Block;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.registry.Registries;
+import net.minecraft.registry.RegistryKey;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelResolver;
+import net.fabricmc.fabric.api.event.Event;
+import net.fabricmc.fabric.api.event.EventFactory;
+
+public class ModelLoaderPluginContextImpl implements ModelLoadingPlugin.Context {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoaderPluginContextImpl.class);
+
+ final Set extraModels = new LinkedHashSet<>();
+
+ private final Map blockStateResolvers = new HashMap<>();
+ private final BlockKey lookupKey = new BlockKey();
+
+ private final Event modelResolvers = EventFactory.createArrayBacked(ModelResolver.class, resolvers -> context -> {
+ for (ModelResolver resolver : resolvers) {
+ try {
+ UnbakedModel model = resolver.resolveModel(context);
+
+ if (model != null) {
+ return model;
+ }
+ } catch (Exception exception) {
+ LOGGER.error("Failed to resolve model", exception);
+ }
+ }
+
+ return null;
+ });
+
+ private static final Identifier[] MODEL_MODIFIER_PHASES = new Identifier[] { ModelModifier.OVERRIDE_PHASE, ModelModifier.DEFAULT_PHASE, ModelModifier.WRAP_PHASE, ModelModifier.WRAP_LAST_PHASE };
+
+ private final Event onLoadModifiers = EventFactory.createWithPhases(ModelModifier.OnLoad.class, modifiers -> (model, context) -> {
+ for (ModelModifier.OnLoad modifier : modifiers) {
+ try {
+ model = modifier.modifyModelOnLoad(model, context);
+ } catch (Exception exception) {
+ LOGGER.error("Failed to modify unbaked model on load", exception);
+ }
+ }
+
+ return model;
+ }, MODEL_MODIFIER_PHASES);
+ private final Event 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 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);
+
+ /**
+ * This field is used by the v0 wrapper to avoid constantly wrapping the context in hot code.
+ */
+ public final Function modelGetter;
+
+ public ModelLoaderPluginContextImpl(Function modelGetter) {
+ this.modelGetter = modelGetter;
+ }
+
+ @Override
+ public void addModels(Identifier... ids) {
+ for (Identifier id : ids) {
+ extraModels.add(id);
+ }
+ }
+
+ @Override
+ public void addModels(Collection extends Identifier> ids) {
+ extraModels.addAll(ids);
+ }
+
+ @Override
+ public void registerBlockStateResolver(Block block, BlockStateResolver resolver) {
+ Objects.requireNonNull(block, "block cannot be null");
+ Objects.requireNonNull(resolver, "resolver cannot be null");
+
+ Optional> optionalKey = Registries.BLOCK.getKey(block);
+
+ if (optionalKey.isEmpty()) {
+ throw new IllegalArgumentException("Received unregistered block");
+ }
+
+ Identifier blockId = optionalKey.get().getValue();
+ BlockKey key = new BlockKey(blockId.getNamespace(), blockId.getPath());
+ BlockStateResolverHolder holder = new BlockStateResolverHolder(resolver, block, blockId);
+
+ if (blockStateResolvers.put(key, holder) != null) {
+ throw new IllegalArgumentException("Duplicate block state resolver for block " + blockId);
+ }
+ }
+
+ @Nullable
+ BlockStateResolverHolder getBlockStateResolver(ModelIdentifier modelId) {
+ BlockKey key = lookupKey;
+ key.namespace = modelId.getNamespace();
+ key.path = modelId.getPath();
+
+ return blockStateResolvers.get(key);
+ }
+
+ @Override
+ public Event resolveModel() {
+ return modelResolvers;
+ }
+
+ @Override
+ public Event modifyModelOnLoad() {
+ return onLoadModifiers;
+ }
+
+ @Override
+ public Event modifyModelBeforeBake() {
+ return beforeBakeModifiers;
+ }
+
+ @Override
+ public Event modifyModelAfterBake() {
+ return afterBakeModifiers;
+ }
+
+ private static class BlockKey {
+ private String namespace;
+ private String path;
+
+ private BlockKey() {
+ }
+
+ private BlockKey(String namespace, String path) {
+ this.namespace = namespace;
+ this.path = path;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ BlockKey blockKey = (BlockKey) o;
+ return namespace.equals(blockKey.namespace) && path.equals(blockKey.path);
+ }
+
+ @Override
+ public int hashCode() {
+ return 31 * namespace.hashCode() + path.hashCode();
+ }
+ }
+
+ // Legacy v0 bridge - remove if the legacy v0 module is removed.
+
+ private final Event legacyVariantProviders = EventFactory.createArrayBacked(LegacyModelVariantProvider.class, providers -> modelId -> {
+ for (LegacyModelVariantProvider provider : providers) {
+ try {
+ UnbakedModel model = provider.loadModelVariant(modelId);
+
+ if (model != null) {
+ return model;
+ }
+ } catch (Exception exception) {
+ LOGGER.error("Failed to run legacy model variant provider", exception);
+ }
+ }
+
+ return null;
+ });
+
+ public Event legacyVariantProviders() {
+ return legacyVariantProviders;
+ }
+}
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
new file mode 100644
index 0000000000..fba9dc19ac
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingEventDispatcher.java
@@ -0,0 +1,465 @@
+/*
+ * 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 java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+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 it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ReferenceSet;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+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.ModelBakeSettings;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.client.texture.Sprite;
+import net.minecraft.client.util.ModelIdentifier;
+import net.minecraft.client.util.SpriteIdentifier;
+import net.minecraft.util.Identifier;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelModifier;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelResolver;
+
+public class ModelLoadingEventDispatcher {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoadingEventDispatcher.class);
+
+ private final ModelLoader loader;
+ private final ModelLoaderPluginContextImpl pluginContext;
+
+ private final ObjectArrayList modelResolverContextStack = new ObjectArrayList<>();
+
+ private final ObjectArrayList blockStateResolverContextStack = new ObjectArrayList<>();
+ private final ReferenceSet resolvingBlocks = new ReferenceOpenHashSet<>();
+
+ private final ObjectArrayList onLoadModifierContextStack = new ObjectArrayList<>();
+ private final ObjectArrayList beforeBakeModifierContextStack = new ObjectArrayList<>();
+ private final ObjectArrayList afterBakeModifierContextStack = new ObjectArrayList<>();
+
+ public ModelLoadingEventDispatcher(ModelLoader loader, List plugins) {
+ this.loader = loader;
+ this.pluginContext = new ModelLoaderPluginContextImpl(((ModelLoaderHooks) loader)::fabric_getOrLoadModel);
+
+ for (ModelLoadingPlugin plugin : plugins) {
+ try {
+ plugin.onInitializeModelLoader(pluginContext);
+ } catch (Exception exception) {
+ LOGGER.error("Failed to initialize model loading plugin", exception);
+ }
+ }
+ }
+
+ public void addExtraModels(Consumer extraModelConsumer) {
+ for (Identifier id : pluginContext.extraModels) {
+ extraModelConsumer.accept(id);
+ }
+ }
+
+ /**
+ * @return {@code true} to cancel the vanilla method
+ */
+ public boolean loadModel(Identifier id) {
+ if (id instanceof ModelIdentifier modelId) {
+ if ("inventory".equals(modelId.getVariant())) {
+ // We ALWAYS override the vanilla inventory model code path entirely, even for vanilla item models.
+ // See loadItemModel for an explanation.
+ loadItemModel(modelId);
+ return true;
+ } else {
+ // Prioritize block state resolver over legacy variant provider
+ BlockStateResolverHolder resolver = pluginContext.getBlockStateResolver(modelId);
+
+ if (resolver != null) {
+ loadBlockStateModels(resolver.resolver(), resolver.block(), resolver.blockId());
+ return true;
+ }
+
+ UnbakedModel legacyModel = legacyLoadModelVariant(modelId);
+
+ if (legacyModel != null) {
+ ((ModelLoaderHooks) loader).fabric_putModel(id, legacyModel);
+ return true;
+ }
+
+ return false;
+ }
+ } else {
+ UnbakedModel model = resolveModel(id);
+
+ if (model != null) {
+ ((ModelLoaderHooks) loader).fabric_putModel(id, model);
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ @Nullable
+ private UnbakedModel legacyLoadModelVariant(ModelIdentifier modelId) {
+ return pluginContext.legacyVariantProviders().invoker().loadModelVariant(modelId);
+ }
+
+ /**
+ * This function handles both modded item models and vanilla item models.
+ * The vanilla code path for item models is never used.
+ * See the long comment in the function for an explanation.
+ */
+ private void loadItemModel(ModelIdentifier modelId) {
+ ModelLoaderHooks loaderHooks = (ModelLoaderHooks) loader;
+
+ Identifier id = modelId.withPrefixedPath("item/");
+
+ // Legacy variant provider
+ UnbakedModel model = legacyLoadModelVariant(modelId);
+
+ // Model resolver
+ if (model == null) {
+ model = resolveModel(id);
+ }
+
+ // Load from the vanilla code path otherwise.
+ if (model == null) {
+ model = loaderHooks.fabric_loadModelFromJson(id);
+ }
+
+ // This is a bit tricky:
+ // We have a single UnbakedModel now, but there are two identifiers:
+ // the ModelIdentifier (...#inventory) and the Identifier (...:item/...).
+ // So we call the on load modifier now and then directly add the model to the ModelLoader,
+ // reimplementing the behavior of ModelLoader#put.
+ // Calling ModelLoader#put is not an option as the model for the Identifier would not be replaced by an on load modifier.
+ // This is why we override the vanilla code path entirely.
+ model = modifyModelOnLoad(modelId, model);
+
+ loaderHooks.fabric_putModelDirectly(modelId, model);
+ loaderHooks.fabric_putModelDirectly(id, model);
+ loaderHooks.fabric_queueModelDependencies(model);
+ }
+
+ private void loadBlockStateModels(BlockStateResolver resolver, Block block, Identifier blockId) {
+ if (!resolvingBlocks.add(block)) {
+ throw new IllegalStateException("Circular reference while resolving models for block " + block);
+ }
+
+ try {
+ resolveBlockStates(resolver, block, blockId);
+ } finally {
+ resolvingBlocks.remove(block);
+ }
+ }
+
+ private void resolveBlockStates(BlockStateResolver resolver, Block block, Identifier blockId) {
+ // Get and prepare context
+ if (blockStateResolverContextStack.isEmpty()) {
+ blockStateResolverContextStack.add(new BlockStateResolverContext());
+ }
+
+ BlockStateResolverContext context = blockStateResolverContextStack.pop();
+ context.prepare(block);
+
+ Reference2ReferenceMap resolvedModels = context.models;
+ ImmutableList allStates = block.getStateManager().getStates();
+ boolean thrown = false;
+
+ // Call resolver
+ try {
+ resolver.resolveBlockStates(context);
+ } catch (Exception e) {
+ LOGGER.error("Failed to resolve block state models for block {}. Using missing model for all states.", block, e);
+ thrown = true;
+ }
+
+ // Copy models over to the loader
+ if (thrown) {
+ UnbakedModel missingModel = ((ModelLoaderHooks) loader).fabric_getMissingModel();
+
+ for (BlockState state : allStates) {
+ ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+ ((ModelLoaderHooks) loader).fabric_putModelDirectly(modelId, missingModel);
+ }
+ } else if (resolvedModels.size() == allStates.size()) {
+ // If there are as many resolved models as total states, all states have
+ // been resolved and models do not need to be null-checked.
+ resolvedModels.forEach((state, model) -> {
+ ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+ ((ModelLoaderHooks) loader).fabric_putModel(modelId, model);
+ });
+ } else {
+ UnbakedModel missingModel = ((ModelLoaderHooks) loader).fabric_getMissingModel();
+
+ for (BlockState state : allStates) {
+ ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
+ @Nullable
+ UnbakedModel model = resolvedModels.get(state);
+
+ if (model == null) {
+ LOGGER.error("Block state resolver did not provide a model for state {} in block {}. Using missing model.", state, block);
+ ((ModelLoaderHooks) loader).fabric_putModelDirectly(modelId, missingModel);
+ } else {
+ ((ModelLoaderHooks) loader).fabric_putModel(modelId, model);
+ }
+ }
+ }
+
+ resolvedModels.clear();
+
+ // Store context for reuse
+ blockStateResolverContextStack.add(context);
+ }
+
+ @Nullable
+ private UnbakedModel resolveModel(Identifier id) {
+ if (modelResolverContextStack.isEmpty()) {
+ modelResolverContextStack.add(new ModelResolverContext());
+ }
+
+ ModelResolverContext context = modelResolverContextStack.pop();
+ context.prepare(id);
+
+ UnbakedModel model = pluginContext.resolveModel().invoker().resolveModel(context);
+
+ modelResolverContextStack.push(context);
+ return model;
+ }
+
+ public UnbakedModel modifyModelOnLoad(Identifier id, UnbakedModel model) {
+ if (onLoadModifierContextStack.isEmpty()) {
+ onLoadModifierContextStack.add(new OnLoadModifierContext());
+ }
+
+ OnLoadModifierContext context = onLoadModifierContextStack.pop();
+ context.prepare(id);
+
+ model = pluginContext.modifyModelOnLoad().invoker().modifyModelOnLoad(model, context);
+
+ onLoadModifierContextStack.push(context);
+ return model;
+ }
+
+ public UnbakedModel modifyModelBeforeBake(UnbakedModel model, Identifier id, Function textureGetter, ModelBakeSettings settings, Baker baker) {
+ if (beforeBakeModifierContextStack.isEmpty()) {
+ beforeBakeModifierContextStack.add(new BeforeBakeModifierContext());
+ }
+
+ BeforeBakeModifierContext context = beforeBakeModifierContextStack.pop();
+ context.prepare(id, textureGetter, settings, baker);
+
+ model = pluginContext.modifyModelBeforeBake().invoker().modifyModelBeforeBake(model, context);
+
+ beforeBakeModifierContextStack.push(context);
+ return model;
+ }
+
+ public BakedModel modifyModelAfterBake(BakedModel model, Identifier id, UnbakedModel sourceModel, Function textureGetter, ModelBakeSettings settings, Baker baker) {
+ if (afterBakeModifierContextStack.isEmpty()) {
+ afterBakeModifierContextStack.add(new AfterBakeModifierContext());
+ }
+
+ AfterBakeModifierContext context = afterBakeModifierContextStack.pop();
+ context.prepare(id, sourceModel, textureGetter, settings, baker);
+
+ model = pluginContext.modifyModelAfterBake().invoker().modifyModelAfterBake(model, context);
+
+ afterBakeModifierContextStack.push(context);
+ return model;
+ }
+
+ private class ModelResolverContext implements ModelResolver.Context {
+ private Identifier id;
+
+ private void prepare(Identifier id) {
+ this.id = id;
+ }
+
+ @Override
+ public Identifier id() {
+ return id;
+ }
+
+ @Override
+ public UnbakedModel getOrLoadModel(Identifier id) {
+ return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+ }
+
+ @Override
+ public ModelLoader loader() {
+ return loader;
+ }
+ }
+
+ private class BlockStateResolverContext implements BlockStateResolver.Context {
+ private Block block;
+ private final Reference2ReferenceMap models = new Reference2ReferenceOpenHashMap<>();
+
+ private void prepare(Block block) {
+ this.block = block;
+ models.clear();
+ }
+
+ @Override
+ public Block block() {
+ return block;
+ }
+
+ @Override
+ public void setModel(BlockState state, UnbakedModel model) {
+ Objects.requireNonNull(model, "state cannot be null");
+ Objects.requireNonNull(model, "model cannot be null");
+
+ if (!state.isOf(block)) {
+ throw new IllegalArgumentException("Attempted to set model for state " + state + " on block " + block);
+ }
+
+ if (models.putIfAbsent(state, model) != null) {
+ throw new IllegalStateException("Duplicate model for state " + state + " on block " + block);
+ }
+ }
+
+ @Override
+ public UnbakedModel getOrLoadModel(Identifier id) {
+ return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+ }
+
+ @Override
+ public ModelLoader loader() {
+ return loader;
+ }
+ }
+
+ private class OnLoadModifierContext implements ModelModifier.OnLoad.Context {
+ private Identifier id;
+
+ private void prepare(Identifier id) {
+ this.id = id;
+ }
+
+ @Override
+ public Identifier id() {
+ return id;
+ }
+
+ @Override
+ public UnbakedModel getOrLoadModel(Identifier id) {
+ return ((ModelLoaderHooks) loader).fabric_getOrLoadModel(id);
+ }
+
+ @Override
+ public ModelLoader loader() {
+ return loader;
+ }
+ }
+
+ private class BeforeBakeModifierContext implements ModelModifier.BeforeBake.Context {
+ private Identifier id;
+ private Function textureGetter;
+ private ModelBakeSettings settings;
+ private Baker baker;
+
+ private void prepare(Identifier id, Function textureGetter, ModelBakeSettings settings, Baker baker) {
+ this.id = id;
+ this.textureGetter = textureGetter;
+ this.settings = settings;
+ this.baker = baker;
+ }
+
+ @Override
+ public Identifier id() {
+ return id;
+ }
+
+ @Override
+ public Function textureGetter() {
+ return textureGetter;
+ }
+
+ @Override
+ public ModelBakeSettings settings() {
+ return settings;
+ }
+
+ @Override
+ public Baker baker() {
+ return baker;
+ }
+
+ @Override
+ public ModelLoader loader() {
+ return loader;
+ }
+ }
+
+ private class AfterBakeModifierContext implements ModelModifier.AfterBake.Context {
+ private Identifier id;
+ private UnbakedModel sourceModel;
+ private Function textureGetter;
+ private ModelBakeSettings settings;
+ private Baker baker;
+
+ private void prepare(Identifier id, UnbakedModel sourceModel, Function textureGetter, ModelBakeSettings settings, Baker baker) {
+ this.id = id;
+ this.sourceModel = sourceModel;
+ this.textureGetter = textureGetter;
+ this.settings = settings;
+ this.baker = baker;
+ }
+
+ @Override
+ public Identifier id() {
+ return id;
+ }
+
+ @Override
+ public UnbakedModel sourceModel() {
+ return sourceModel;
+ }
+
+ @Override
+ public Function textureGetter() {
+ return textureGetter;
+ }
+
+ @Override
+ public ModelBakeSettings settings() {
+ return settings;
+ }
+
+ @Override
+ public Baker baker() {
+ return baker;
+ }
+
+ @Override
+ public ModelLoader loader() {
+ return loader;
+ }
+ }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java
new file mode 100644
index 0000000000..8ccbee0264
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/impl/client/model/loading/ModelLoadingPluginManager.java
@@ -0,0 +1,77 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.util.Util;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.api.client.model.loading.v1.PreparableModelLoadingPlugin;
+
+public final class ModelLoadingPluginManager {
+ private static final List PLUGINS = new ArrayList<>();
+ private static final List> PREPARABLE_PLUGINS = new ArrayList<>();
+
+ public static final ThreadLocal> CURRENT_PLUGINS = new ThreadLocal<>();
+
+ public static void registerPlugin(ModelLoadingPlugin plugin) {
+ Objects.requireNonNull(plugin, "plugin must not be null");
+
+ PLUGINS.add(plugin);
+ }
+
+ public static void registerPlugin(PreparableModelLoadingPlugin.DataLoader loader, PreparableModelLoadingPlugin plugin) {
+ Objects.requireNonNull(loader, "data loader must not be null");
+ Objects.requireNonNull(plugin, "plugin must not be null");
+
+ PREPARABLE_PLUGINS.add(new PreparablePluginHolder<>(loader, plugin));
+ }
+
+ /**
+ * The current exception behavior as of 1.20 is as follows.
+ * If getting a {@link CompletableFuture}s throws then the whole client will crash.
+ * If a {@link CompletableFuture} completes exceptionally then the resource reload will fail.
+ */
+ public static CompletableFuture> preparePlugins(ResourceManager resourceManager, Executor executor) {
+ List> futures = new ArrayList<>();
+
+ for (ModelLoadingPlugin plugin : PLUGINS) {
+ futures.add(CompletableFuture.completedFuture(plugin));
+ }
+
+ for (PreparablePluginHolder> holder : PREPARABLE_PLUGINS) {
+ futures.add(preparePlugin(holder, resourceManager, executor));
+ }
+
+ return Util.combine(futures);
+ }
+
+ private static CompletableFuture preparePlugin(PreparablePluginHolder holder, ResourceManager resourceManager, Executor executor) {
+ CompletableFuture dataFuture = holder.loader.load(resourceManager, executor);
+ return dataFuture.thenApply(data -> pluginContext -> holder.plugin.onInitializeModelLoader(data, pluginContext));
+ }
+
+ private ModelLoadingPluginManager() { }
+
+ private record PreparablePluginHolder(PreparableModelLoadingPlugin.DataLoader loader, PreparableModelLoadingPlugin plugin) { }
+}
diff --git a/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java
new file mode 100644
index 0000000000..d627eb951f
--- /dev/null
+++ b/fabric-model-loading-api-v1/src/client/java/net/fabricmc/fabric/mixin/client/model/loading/BakedModelManagerMixin.java
@@ -0,0 +1,82 @@
+/*
+ * 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 java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.Executor;
+import java.util.function.BiFunction;
+
+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.Redirect;
+
+import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.ModelLoader;
+import net.minecraft.client.render.model.json.JsonUnbakedModel;
+import net.minecraft.resource.ResourceManager;
+import net.minecraft.resource.ResourceReloader;
+import net.minecraft.util.Identifier;
+import net.minecraft.util.Pair;
+import net.minecraft.util.profiler.Profiler;
+
+import net.fabricmc.fabric.api.client.model.loading.v1.FabricBakedModelManager;
+import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
+import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
+
+@Mixin(BakedModelManager.class)
+public class BakedModelManagerMixin implements FabricBakedModelManager {
+ @Shadow
+ private Map models;
+
+ @Override
+ public BakedModel getModel(Identifier id) {
+ return models.get(id);
+ }
+
+ @Redirect(
+ method = "reload",
+ at = @At(
+ value = "INVOKE",
+ target = "java/util/concurrent/CompletableFuture.thenCombineAsync(Ljava/util/concurrent/CompletionStage;Ljava/util/function/BiFunction;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;",
+ remap = false
+ ),
+ allow = 1)
+ private CompletableFuture loadModelPluginData(
+ CompletableFuture