Port Model Loading API v1 to 1.21 (#3824)

* Port Model Loading API v1

* Fix checkstyle and address review comments
This commit is contained in:
PepperCode1 2024-06-08 06:02:54 -07:00 committed by GitHub
parent d1321076fe
commit fe474d6b05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 469 additions and 444 deletions

View file

@ -4,6 +4,7 @@ moduleDependencies(project, ['fabric-api-base'])
testDependencies(project, [
':fabric-renderer-api-v1',
':fabric-renderer-indigo',
':fabric-rendering-v1',
':fabric-resource-loader-v0'
])

View file

@ -22,7 +22,6 @@ 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;
/**
@ -72,7 +71,7 @@ public interface BlockStateResolver {
void setModel(BlockState state, UnbakedModel model);
/**
* Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
* Loads a model using an {@link Identifier}, 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
@ -81,9 +80,6 @@ public interface BlockStateResolver {
/**
* The current model loader instance, which changes between resource reloads.
*
* <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
* use {@link #getOrLoadModel} from the context instead.
*/
ModelLoader loader();
}

View file

@ -27,7 +27,6 @@ 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;
@ -43,7 +42,7 @@ public final class DelegatingUnbakedModel implements UnbakedModel {
/**
* Constructs a new delegating model.
*
* @param delegate The identifier (can be a {@link ModelIdentifier}) of the underlying baked model.
* @param delegate The identifier of the underlying baked model.
*/
public DelegatingUnbakedModel(Identifier delegate) {
this.delegate = delegate;
@ -57,11 +56,12 @@ public final class DelegatingUnbakedModel implements UnbakedModel {
@Override
public void setParents(Function<Identifier, UnbakedModel> modelLoader) {
modelLoader.apply(delegate).setParents(modelLoader);
}
@Nullable
@Override
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer, Identifier modelId) {
@Nullable
public BakedModel bake(Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings rotationContainer) {
return baker.bake(delegate, rotationContainer);
}
}

View file

@ -16,8 +16,6 @@
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;
@ -30,20 +28,18 @@ import net.minecraft.util.Identifier;
*/
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.
* Similar to {@link BakedModelManager#getModel(ModelIdentifier)}, but accepts an {@link Identifier} instead of a
* {@link ModelIdentifier}. Use this method to retrieve models loaded using
* {@link ModelLoadingPlugin.Context#addModels}, since those models do not have corresponding
* {@link ModelIdentifier}s.
*
* <p><b>This method, as well as its vanilla counterpart, should only be used after the
* {@link BakedModelManager} has completed reloading.</b> Otherwise, the result will be
* outdated or null.
* outdated or an exception will be thrown.
*
* @param id the id of the model
* @return the model
*/
@Nullable
default BakedModel getModel(Identifier id) {
throw new UnsupportedOperationException("Implemented via mixin.");
}

View file

@ -22,7 +22,6 @@ 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;
@ -53,13 +52,14 @@ public interface ModelLoadingPlugin {
@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.
* Adds one or more models that will be loaded, baked, and made available through
* {@link FabricBakedModelManager#getModel(Identifier)}.
*/
void addModels(Identifier... ids);
/**
* Adds multiple models (can be {@link ModelIdentifier}s) to the list of models that will be loaded and baked.
* Adds multiple models that will be loaded, baked, and made available through
* {@link FabricBakedModelManager#getModel(Identifier)}.
*/
void addModels(Collection<? extends Identifier> ids);

View file

@ -20,6 +20,7 @@ import java.util.function.Function;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.Baker;
@ -39,7 +40,7 @@ import net.fabricmc.fabric.api.event.Event;
*
* <p>Example use cases:
* <ul>
* <li>Overriding a model for a particular block state - check if the given identifier is a {@link ModelIdentifier},
* <li>Overriding a model for a particular block state - check if the given top-level identifier is not null,
* and then check if it has the appropriate variant for that block state. If so, return your desired model,
* otherwise return the given model.</li>
* <li>Wrapping a model to override certain behaviors - simply return a new model instance and delegate calls
@ -91,16 +92,26 @@ public final class ModelModifier {
@ApiStatus.NonExtendable
interface Context {
/**
* The identifier of this model (may be a {@link ModelIdentifier}).
* Models with a resource ID are loaded directly from JSON or a {@link ModelModifier}.
*
* <p>For item models, only the {@link ModelIdentifier} with the {@code inventory} variant is passed, and
* not the corresponding plain identifier.
* @return the identifier of the given model as an {@link Identifier}, or null if {@link #topLevelId()} is
* not null
*/
Identifier id();
@UnknownNullability("#topLevelId() != null")
Identifier resourceId();
/**
* Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already
* loaded.
* Models with a top-level ID are loaded from blockstate files, {@link BlockStateResolver}s, or by copying
* a previously loaded model.
*
* @return the identifier of the given model as a {@link ModelIdentifier}, or null if {@link #resourceId()}
* is not null
*/
@UnknownNullability("#resourceId() != null")
ModelIdentifier topLevelId();
/**
* Loads a model using an {@link Identifier}, 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
@ -109,9 +120,6 @@ public final class ModelModifier {
/**
* The current model loader instance, which changes between resource reloads.
*
* <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model
* resolution; use {@link #getOrLoadModel} from the context instead.
*/
ModelLoader loader();
}
@ -135,9 +143,23 @@ public final class ModelModifier {
@ApiStatus.NonExtendable
interface Context {
/**
* The identifier of this model (may be a {@link ModelIdentifier}).
* Models with a resource ID are loaded directly from JSON or a {@link ModelModifier}.
*
* @return the identifier of the given model as an {@link Identifier}, or null if {@link #topLevelId()} is
* not null
*/
Identifier id();
@UnknownNullability("#topLevelId() != null")
Identifier resourceId();
/**
* Models with a top-level ID are loaded from blockstate files, {@link BlockStateResolver}s, or by copying
* a previously loaded model.
*
* @return the identifier of the given model as a {@link ModelIdentifier}, or null if {@link #resourceId()}
* is not null
*/
@UnknownNullability("#resourceId() != null")
ModelIdentifier topLevelId();
/**
* The function that can be used to retrieve sprites.
@ -189,9 +211,23 @@ public final class ModelModifier {
@ApiStatus.NonExtendable
interface Context {
/**
* The identifier of this model (may be a {@link ModelIdentifier}).
* Models with a resource ID are loaded directly from JSON or a {@link ModelModifier}.
*
* @return the identifier of the given model as an {@link Identifier}, or null if {@link #topLevelId()} is
* not null
*/
Identifier id();
@UnknownNullability("#topLevelId() != null")
Identifier resourceId();
/**
* Models with a top-level ID are loaded from blockstate files, {@link BlockStateResolver}s, or by copying
* a previously loaded model.
*
* @return the identifier of the given model as a {@link ModelIdentifier}, or null if {@link #resourceId()}
* is not null
*/
@UnknownNullability("#resourceId() != null")
ModelIdentifier topLevelId();
/**
* The unbaked model that is being baked.

View file

@ -21,7 +21,6 @@ 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;
/**
@ -64,7 +63,7 @@ public interface ModelResolver {
Identifier id();
/**
* Loads a model using an {@link Identifier} or {@link ModelIdentifier}, or gets it if it was already loaded.
* Loads a model using an {@link Identifier}, 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
@ -73,9 +72,6 @@ public interface ModelResolver {
/**
* The current model loader instance, which changes between resource reloads.
*
* <p>Do <b>not</b> call {@link ModelLoader#getOrLoadModel} as it does not supported nested model resolution;
* use {@link #getOrLoadModel} from the context instead.
*/
ModelLoader loader();
}

View file

@ -16,15 +16,11 @@
package net.fabricmc.fabric.impl.client.model.loading;
import org.jetbrains.annotations.Nullable;
import java.util.function.Function;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.util.ModelIdentifier;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
/**
* Legacy v0 bridge - remove if the legacy v0 module is removed.
*/
public interface LegacyModelVariantProvider {
@Nullable
UnbakedModel loadModelVariant(ModelIdentifier modelId);
public interface BakerImplHooks {
Function<SpriteIdentifier, Sprite> fabric_getTextureGetter();
}

View file

@ -0,0 +1,30 @@
/*
* 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 net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.state.StateManager;
import net.minecraft.util.Identifier;
public interface BlockStatesLoaderHooks {
void fabric_setLoadingOverride(LoadingOverride override);
interface LoadingOverride {
boolean loadBlockStates(Identifier id, StateManager<Block, BlockState> stateManager);
}
}

View file

@ -17,7 +17,7 @@
package net.fabricmc.fabric.impl.client.model.loading;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.render.model.json.JsonUnbakedModel;
import net.minecraft.client.util.ModelIdentifier;
import net.minecraft.util.Identifier;
public interface ModelLoaderHooks {
@ -27,11 +27,5 @@ public interface ModelLoaderHooks {
UnbakedModel fabric_getOrLoadModel(Identifier id);
void fabric_putModel(Identifier id, UnbakedModel model);
void fabric_putModelDirectly(Identifier id, UnbakedModel model);
void fabric_queueModelDependencies(UnbakedModel model);
JsonUnbakedModel fabric_loadModelFromJson(Identifier id);
void fabric_add(ModelIdentifier id, UnbakedModel model);
}

View file

@ -0,0 +1,42 @@
/*
* 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 net.minecraft.client.util.ModelIdentifier;
import net.minecraft.util.Identifier;
public final class ModelLoadingConstants {
/**
* This variant is used to convert user-provided Identifiers for extra models to ModelIdentifiers, since top-level
* models that will be baked must have a ModelIdentifier. Models corresponding to the Identifiers will go through
* ModelModifier.OnLoad, but models corresponding to the ModelIdentifiers will not.
*
* <p>This variant must be non-empty, must not contain "=", and must not be equal to "inventory" or "missingno".
*/
public static final String RESOURCE_SPECIAL_VARIANT = "fabric_resource";
private ModelLoadingConstants() {
}
public static ModelIdentifier toResourceModelId(Identifier id) {
return new ModelIdentifier(id, RESOURCE_SPECIAL_VARIANT);
}
public static boolean isResourceModelId(ModelIdentifier id) {
return id.variant().equals(RESOURCE_SPECIAL_VARIANT);
}
}

View file

@ -25,9 +25,8 @@ 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.jetbrains.annotations.UnknownNullability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -42,6 +41,7 @@ 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.state.StateManager;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.client.model.loading.v1.BlockStateResolver;
@ -53,12 +53,10 @@ public class ModelLoadingEventDispatcher {
private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoadingEventDispatcher.class);
private final ModelLoader loader;
private final ModelLoaderPluginContextImpl pluginContext;
private final ModelLoadingPluginContextImpl pluginContext;
private final ObjectArrayList<ModelResolverContext> modelResolverContextStack = new ObjectArrayList<>();
private final ObjectArrayList<BlockStateResolverContext> blockStateResolverContextStack = new ObjectArrayList<>();
private final ReferenceSet<Block> resolvingBlocks = new ReferenceOpenHashSet<>();
private final BlockStateResolverContext blockStateResolverContext = new BlockStateResolverContext();
private final ObjectArrayList<OnLoadModifierContext> onLoadModifierContextStack = new ObjectArrayList<>();
private final ObjectArrayList<BeforeBakeModifierContext> beforeBakeModifierContextStack = new ObjectArrayList<>();
@ -66,7 +64,7 @@ public class ModelLoadingEventDispatcher {
public ModelLoadingEventDispatcher(ModelLoader loader, List<ModelLoadingPlugin> plugins) {
this.loader = loader;
this.pluginContext = new ModelLoaderPluginContextImpl(((ModelLoaderHooks) loader)::fabric_getOrLoadModel);
this.pluginContext = new ModelLoadingPluginContextImpl();
for (ModelLoadingPlugin plugin : plugins) {
try {
@ -83,107 +81,19 @@ public class ModelLoadingEventDispatcher {
}
}
/**
* @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);
public boolean loadBlockStateModels(Identifier id, StateManager<Block, BlockState> stateManager) {
BlockStateResolver resolver = pluginContext.blockStateResolvers.get(id);
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;
}
if (resolver != null) {
resolveBlockStates(resolver, stateManager.getOwner(), id);
return true;
} 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();
BlockStateResolverContext context = blockStateResolverContext;
context.prepare(block);
Reference2ReferenceMap<BlockState, UnbakedModel> resolvedModels = context.models;
@ -204,14 +114,14 @@ public class ModelLoadingEventDispatcher {
for (BlockState state : allStates) {
ModelIdentifier modelId = BlockModels.getModelId(blockId, state);
((ModelLoaderHooks) loader).fabric_putModelDirectly(modelId, missingModel);
((ModelLoaderHooks) loader).fabric_add(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);
((ModelLoaderHooks) loader).fabric_add(modelId, model);
});
} else {
UnbakedModel missingModel = ((ModelLoaderHooks) loader).fabric_getMissingModel();
@ -223,21 +133,18 @@ public class ModelLoadingEventDispatcher {
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);
((ModelLoaderHooks) loader).fabric_add(modelId, missingModel);
} else {
((ModelLoaderHooks) loader).fabric_putModel(modelId, model);
((ModelLoaderHooks) loader).fabric_add(modelId, model);
}
}
}
resolvedModels.clear();
// Store context for reuse
blockStateResolverContextStack.add(context);
}
@Nullable
private UnbakedModel resolveModel(Identifier id) {
public UnbakedModel resolveModel(Identifier id) {
if (modelResolverContextStack.isEmpty()) {
modelResolverContextStack.add(new ModelResolverContext());
}
@ -251,13 +158,13 @@ public class ModelLoadingEventDispatcher {
return model;
}
public UnbakedModel modifyModelOnLoad(Identifier id, UnbakedModel model) {
public UnbakedModel modifyModelOnLoad(UnbakedModel model, @UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId) {
if (onLoadModifierContextStack.isEmpty()) {
onLoadModifierContextStack.add(new OnLoadModifierContext());
}
OnLoadModifierContext context = onLoadModifierContextStack.pop();
context.prepare(id);
context.prepare(resourceId, topLevelId);
model = pluginContext.modifyModelOnLoad().invoker().modifyModelOnLoad(model, context);
@ -265,13 +172,13 @@ public class ModelLoadingEventDispatcher {
return model;
}
public UnbakedModel modifyModelBeforeBake(UnbakedModel model, Identifier id, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
public UnbakedModel modifyModelBeforeBake(UnbakedModel model, @UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
if (beforeBakeModifierContextStack.isEmpty()) {
beforeBakeModifierContextStack.add(new BeforeBakeModifierContext());
}
BeforeBakeModifierContext context = beforeBakeModifierContextStack.pop();
context.prepare(id, textureGetter, settings, baker);
context.prepare(resourceId, topLevelId, textureGetter, settings, baker);
model = pluginContext.modifyModelBeforeBake().invoker().modifyModelBeforeBake(model, context);
@ -280,13 +187,13 @@ public class ModelLoadingEventDispatcher {
}
@Nullable
public BakedModel modifyModelAfterBake(@Nullable BakedModel model, Identifier id, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
public BakedModel modifyModelAfterBake(@Nullable BakedModel model, @UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
if (afterBakeModifierContextStack.isEmpty()) {
afterBakeModifierContextStack.add(new AfterBakeModifierContext());
}
AfterBakeModifierContext context = afterBakeModifierContextStack.pop();
context.prepare(id, sourceModel, textureGetter, settings, baker);
context.prepare(resourceId, topLevelId, sourceModel, textureGetter, settings, baker);
model = pluginContext.modifyModelAfterBake().invoker().modifyModelAfterBake(model, context);
@ -357,15 +264,26 @@ public class ModelLoadingEventDispatcher {
}
private class OnLoadModifierContext implements ModelModifier.OnLoad.Context {
private Identifier id;
@UnknownNullability
private Identifier resourceId;
@UnknownNullability
private ModelIdentifier topLevelId;
private void prepare(Identifier id) {
this.id = id;
private void prepare(@UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId) {
this.resourceId = resourceId;
this.topLevelId = topLevelId;
}
@Override
public Identifier id() {
return id;
@UnknownNullability("#topLevelId() != null")
public Identifier resourceId() {
return resourceId;
}
@Override
@UnknownNullability("#resourceId() != null")
public ModelIdentifier topLevelId() {
return topLevelId;
}
@Override
@ -380,21 +298,32 @@ public class ModelLoadingEventDispatcher {
}
private class BeforeBakeModifierContext implements ModelModifier.BeforeBake.Context {
private Identifier id;
@UnknownNullability
private Identifier resourceId;
@UnknownNullability
private ModelIdentifier topLevelId;
private Function<SpriteIdentifier, Sprite> textureGetter;
private ModelBakeSettings settings;
private Baker baker;
private void prepare(Identifier id, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
this.id = id;
private void prepare(@UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
this.resourceId = resourceId;
this.topLevelId = topLevelId;
this.textureGetter = textureGetter;
this.settings = settings;
this.baker = baker;
}
@Override
public Identifier id() {
return id;
@UnknownNullability("#topLevelId() != null")
public Identifier resourceId() {
return resourceId;
}
@Override
@UnknownNullability("#resourceId() != null")
public ModelIdentifier topLevelId() {
return topLevelId;
}
@Override
@ -419,14 +348,18 @@ public class ModelLoadingEventDispatcher {
}
private class AfterBakeModifierContext implements ModelModifier.AfterBake.Context {
private Identifier id;
@UnknownNullability
private Identifier resourceId;
@UnknownNullability
private ModelIdentifier topLevelId;
private UnbakedModel sourceModel;
private Function<SpriteIdentifier, Sprite> textureGetter;
private ModelBakeSettings settings;
private Baker baker;
private void prepare(Identifier id, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
this.id = id;
private void prepare(@UnknownNullability Identifier resourceId, @UnknownNullability ModelIdentifier topLevelId, UnbakedModel sourceModel, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Baker baker) {
this.resourceId = resourceId;
this.topLevelId = topLevelId;
this.sourceModel = sourceModel;
this.textureGetter = textureGetter;
this.settings = settings;
@ -434,8 +367,15 @@ public class ModelLoadingEventDispatcher {
}
@Override
public Identifier id() {
return id;
@UnknownNullability("#topLevelId() != null")
public Identifier resourceId() {
return resourceId;
}
@Override
@UnknownNullability("#resourceId() != null")
public ModelIdentifier topLevelId() {
return topLevelId;
}
@Override

View file

@ -23,15 +23,12 @@ 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;
@ -43,13 +40,11 @@ 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);
public class ModelLoadingPluginContextImpl implements ModelLoadingPlugin.Context {
private static final Logger LOGGER = LoggerFactory.getLogger(ModelLoadingPluginContextImpl.class);
final Set<Identifier> extraModels = new LinkedHashSet<>();
private final Map<BlockKey, BlockStateResolverHolder> blockStateResolvers = new HashMap<>();
private final BlockKey lookupKey = new BlockKey();
final Map<Identifier, BlockStateResolver> blockStateResolvers = new HashMap<>();
private final Event<ModelResolver> modelResolvers = EventFactory.createArrayBacked(ModelResolver.class, resolvers -> context -> {
for (ModelResolver resolver : resolvers) {
@ -103,13 +98,7 @@ public class ModelLoaderPluginContextImpl implements ModelLoadingPlugin.Context
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<Identifier, UnbakedModel> modelGetter;
public ModelLoaderPluginContextImpl(Function<Identifier, UnbakedModel> modelGetter) {
this.modelGetter = modelGetter;
public ModelLoadingPluginContextImpl() {
}
@Override
@ -136,23 +125,12 @@ public class ModelLoaderPluginContextImpl implements ModelLoadingPlugin.Context
}
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);
if (blockStateResolvers.put(blockId, resolver) != null) {
throw new IllegalArgumentException("Duplicate block state resolver for " + block);
}
}
@Nullable
BlockStateResolverHolder getBlockStateResolver(ModelIdentifier modelId) {
BlockKey key = lookupKey;
key.namespace = modelId.getNamespace();
key.path = modelId.getPath();
return blockStateResolvers.get(key);
}
@Override
public Event<ModelResolver> resolveModel() {
return modelResolvers;
@ -172,52 +150,4 @@ public class ModelLoaderPluginContextImpl implements ModelLoadingPlugin.Context
public Event<ModelModifier.AfterBake> 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<LegacyModelVariantProvider> 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<LegacyModelVariantProvider> legacyVariantProviders() {
return legacyVariantProviders;
}
}

View file

@ -28,11 +28,12 @@ import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import net.minecraft.class_9824;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.BakedModelManager;
import net.minecraft.client.render.model.BlockStatesLoader;
import net.minecraft.client.render.model.ModelLoader;
import net.minecraft.client.render.model.json.JsonUnbakedModel;
import net.minecraft.client.util.ModelIdentifier;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.ResourceReloader;
import net.minecraft.util.Identifier;
@ -41,16 +42,17 @@ 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.ModelLoadingConstants;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
@Mixin(BakedModelManager.class)
public class BakedModelManagerMixin implements FabricBakedModelManager {
abstract class BakedModelManagerMixin implements FabricBakedModelManager {
@Shadow
private Map<Identifier, BakedModel> models;
private Map<ModelIdentifier, BakedModel> models;
@Override
public BakedModel getModel(Identifier id) {
return models.get(id);
return models.get(ModelLoadingConstants.toResourceModelId(id));
}
@Redirect(
@ -63,8 +65,8 @@ public class BakedModelManagerMixin implements FabricBakedModelManager {
allow = 1)
private CompletableFuture<ModelLoader> loadModelPluginData(
CompletableFuture<Map<Identifier, JsonUnbakedModel>> self,
CompletionStage<Map<Identifier, List<class_9824.SourceTrackedData>>> otherFuture,
BiFunction<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<class_9824.SourceTrackedData>>, ModelLoader> modelLoaderConstructor,
CompletionStage<Map<Identifier, List<BlockStatesLoader.SourceTrackedData>>> otherFuture,
BiFunction<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<BlockStatesLoader.SourceTrackedData>>, ModelLoader> modelLoaderConstructor,
Executor executor,
// reload args
ResourceReloader.Synchronizer synchronizer,
@ -74,10 +76,12 @@ public class BakedModelManagerMixin implements FabricBakedModelManager {
Executor prepareExecutor,
Executor applyExecutor) {
CompletableFuture<List<ModelLoadingPlugin>> pluginsFuture = ModelLoadingPluginManager.preparePlugins(manager, prepareExecutor);
CompletableFuture<Pair<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<class_9824.SourceTrackedData>>>> pairFuture = self.thenCombine(otherFuture, Pair::new);
CompletableFuture<Pair<Map<Identifier, JsonUnbakedModel>, Map<Identifier, List<BlockStatesLoader.SourceTrackedData>>>> pairFuture = self.thenCombine(otherFuture, Pair::new);
return pairFuture.thenCombineAsync(pluginsFuture, (pair, plugins) -> {
ModelLoadingPluginManager.CURRENT_PLUGINS.set(plugins);
return modelLoaderConstructor.apply(pair.getLeft(), pair.getRight());
ModelLoader modelLoader = modelLoaderConstructor.apply(pair.getLeft(), pair.getRight());
ModelLoadingPluginManager.CURRENT_PLUGINS.remove();
return modelLoader;
}, executor);
}
}

View file

@ -0,0 +1,51 @@
/*
* 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 org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.block.Block;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.model.BlockStatesLoader;
import net.minecraft.state.StateManager;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.client.model.loading.BlockStatesLoaderHooks;
@Mixin(BlockStatesLoader.class)
abstract class BlockStatesLoaderMixin implements BlockStatesLoaderHooks {
@Unique
@Nullable
private LoadingOverride loadingOverride;
@Inject(method = "loadBlockStates(Lnet/minecraft/util/Identifier;Lnet/minecraft/state/StateManager;)V", at = @At("HEAD"), cancellable = true)
private void onHeadLoadBlockStates(Identifier id, StateManager<Block, BlockState> stateManager, CallbackInfo ci) {
if (loadingOverride != null && loadingOverride.loadBlockStates(id, stateManager)) {
ci.cancel();
}
}
@Override
public void fabric_setLoadingOverride(LoadingOverride override) {
loadingOverride = override;
}
}

View file

@ -18,28 +18,29 @@ package net.fabricmc.fabric.mixin.client.model.loading;
import java.util.function.Function;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.Coerce;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.Baker;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.render.model.ModelLoader;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.render.model.json.JsonUnbakedModel;
import net.minecraft.client.texture.Sprite;
import net.minecraft.client.util.SpriteIdentifier;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.client.model.loading.BakerImplHooks;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderHooks;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
@Mixin(targets = "net/minecraft/client/render/model/ModelLoader$BakerImpl")
public class ModelLoaderBakerImplMixin {
abstract class ModelLoaderBakerImplMixin implements BakerImplHooks {
@Shadow
@Final
private ModelLoader field_40571;
@ -47,23 +48,16 @@ public class ModelLoaderBakerImplMixin {
@Final
private Function<SpriteIdentifier, Sprite> textureGetter;
@ModifyVariable(method = "bake", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/render/model/ModelLoader$BakerImpl;getOrLoadModel(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/UnbakedModel;"))
private UnbakedModel invokeModifyBeforeBake(UnbakedModel model, Identifier id, ModelBakeSettings settings) {
@WrapOperation(method = "bake(Lnet/minecraft/util/Identifier;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ModelLoader$BakerImpl;bake(Lnet/minecraft/client/render/model/UnbakedModel;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;"))
private BakedModel wrapInnerBake(@Coerce Baker self, UnbakedModel unbakedModel, ModelBakeSettings settings, Operation<BakedModel> operation, Identifier id) {
ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
return dispatcher.modifyModelBeforeBake(model, id, textureGetter, settings, (Baker) this);
unbakedModel = dispatcher.modifyModelBeforeBake(unbakedModel, id, null, textureGetter, settings, self);
BakedModel model = operation.call(self, unbakedModel, settings);
return dispatcher.modifyModelAfterBake(model, id, null, unbakedModel, textureGetter, settings, self);
}
@Redirect(method = "bake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/UnbakedModel;bake(Lnet/minecraft/client/render/model/Baker;Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/BakedModel;"))
private BakedModel invokeModifyAfterBake(UnbakedModel unbakedModel, Baker baker, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Identifier id) {
BakedModel model = unbakedModel.bake(baker, textureGetter, settings, id);
ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
return dispatcher.modifyModelAfterBake(model, id, unbakedModel, textureGetter, settings, baker);
}
@Redirect(method = "bake", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/json/JsonUnbakedModel;bake(Lnet/minecraft/client/render/model/Baker;Lnet/minecraft/client/render/model/json/JsonUnbakedModel;Ljava/util/function/Function;Lnet/minecraft/client/render/model/ModelBakeSettings;Lnet/minecraft/util/Identifier;Z)Lnet/minecraft/client/render/model/BakedModel;"))
private BakedModel invokeModifyAfterBake(JsonUnbakedModel unbakedModel, Baker baker, JsonUnbakedModel parent, Function<SpriteIdentifier, Sprite> textureGetter, ModelBakeSettings settings, Identifier id, boolean hasDepth) {
BakedModel model = unbakedModel.bake(baker, parent, textureGetter, settings, id, hasDepth);
ModelLoadingEventDispatcher dispatcher = ((ModelLoaderHooks) this.field_40571).fabric_getDispatcher();
return dispatcher.modifyModelAfterBake(model, id, unbakedModel, textureGetter, settings, baker);
@Override
public Function<SpriteIdentifier, Sprite> fabric_getTextureGetter() {
return textureGetter;
}
}

View file

@ -19,36 +19,48 @@ package net.fabricmc.fabric.mixin.client.model.loading;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.class_9824;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.client.render.model.Baker;
import net.minecraft.client.render.model.BlockStatesLoader;
import net.minecraft.client.render.model.ModelBakeSettings;
import net.minecraft.client.render.model.ModelLoader;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.client.render.model.json.JsonUnbakedModel;
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.minecraft.util.profiler.Profiler;
import net.fabricmc.fabric.impl.client.model.loading.BakerImplHooks;
import net.fabricmc.fabric.impl.client.model.loading.BlockStatesLoaderHooks;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoaderHooks;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingConstants;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingEventDispatcher;
import net.fabricmc.fabric.impl.client.model.loading.ModelLoadingPluginManager;
@Mixin(ModelLoader.class)
public abstract class ModelLoaderMixin implements ModelLoaderHooks {
// The missing model is always loaded and added first.
@Final
@Shadow
public static ModelIdentifier MISSING_ID;
abstract class ModelLoaderMixin implements ModelLoaderHooks {
@Final
@Shadow
private Set<Identifier> modelsToLoad;
@ -57,94 +69,119 @@ public abstract class ModelLoaderMixin implements ModelLoaderHooks {
private Map<Identifier, UnbakedModel> unbakedModels;
@Shadow
@Final
private Map<Identifier, UnbakedModel> modelsToBake;
private Map<ModelIdentifier, UnbakedModel> modelsToBake;
@Shadow
@Final
private UnbakedModel missingModel;
@Unique
private ModelLoadingEventDispatcher fabric_eventDispatcher;
// Explicitly not @Unique to allow mods that heavily rework model loading to reimplement the guard.
// Note that this is an implementation detail; it can change at any time.
private int fabric_guardGetOrLoadModel = 0;
private boolean fabric_enableGetOrLoadModelGuard = true;
@Unique
private final ObjectLinkedOpenHashSet<Identifier> modelLoadingStack = new ObjectLinkedOpenHashSet<>();
@Shadow
private void addModel(ModelIdentifier id) {
}
abstract UnbakedModel getOrLoadModel(Identifier id);
@Shadow
public abstract UnbakedModel getOrLoadModel(Identifier id);
abstract void add(ModelIdentifier id, UnbakedModel unbakedModel);
@Shadow
private void loadModel(Identifier id) {
}
abstract JsonUnbakedModel loadModelFromJson(Identifier id);
@Shadow
private void method_61076(ModelIdentifier id, UnbakedModel unbakedModel) {
}
@Shadow
public abstract JsonUnbakedModel loadModelFromJson(Identifier id);
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "net/minecraft/util/profiler/Profiler.swap(Ljava/lang/String;)V", ordinal = 0))
private void afterMissingModelInit(BlockColors blockColors, Profiler profiler, Map<Identifier, JsonUnbakedModel> jsonUnbakedModels, Map<Identifier, List<class_9824.SourceTrackedData>> blockStates, CallbackInfo info) {
@Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/BlockStatesLoader;load()V"))
private void afterMissingModelInit(BlockColors blockColors, Profiler profiler, Map<Identifier, JsonUnbakedModel> jsonUnbakedModels, Map<Identifier, List<BlockStatesLoader.SourceTrackedData>> blockStates, CallbackInfo info, @Local BlockStatesLoader blockStatesLoader) {
// Sanity check
if (!unbakedModels.containsKey(MISSING_ID)) {
if (missingModel == null || !modelsToBake.containsKey(ModelLoader.MISSING_MODEL_ID)) {
throw new AssertionError("Missing model not initialized. This is likely a Fabric API porting bug.");
}
// Add the missing model to the cache since vanilla doesn't. Mods may load/bake the missing model directly.
unbakedModels.put(ModelLoader.MISSING_ID, missingModel);
profiler.swap("fabric_plugins_init");
fabric_eventDispatcher = new ModelLoadingEventDispatcher((ModelLoader) (Object) this, ModelLoadingPluginManager.CURRENT_PLUGINS.get());
ModelLoadingPluginManager.CURRENT_PLUGINS.remove();
fabric_eventDispatcher.addExtraModels(this::addModel);
fabric_eventDispatcher.addExtraModels(this::addExtraModel);
((BlockStatesLoaderHooks) blockStatesLoader).fabric_setLoadingOverride(fabric_eventDispatcher::loadBlockStateModels);
}
@Unique
private void addModel(Identifier id) {
if (id instanceof ModelIdentifier) {
addModel((ModelIdentifier) id);
} else {
// The vanilla addModel method is arbitrarily limited to ModelIdentifiers,
// but it's useful to tell the game to just load and bake a direct model path as well.
// Replicate the vanilla logic of addModel here.
UnbakedModel unbakedModel = getOrLoadModel(id);
this.unbakedModels.put(id, unbakedModel);
this.modelsToBake.put(id, unbakedModel);
}
private void addExtraModel(Identifier id) {
ModelIdentifier modelId = ModelLoadingConstants.toResourceModelId(id);
UnbakedModel unbakedModel = getOrLoadModel(id);
add(modelId, unbakedModel);
}
@Inject(method = "getOrLoadModel", at = @At("HEAD"))
private void fabric_preventNestedGetOrLoadModel(Identifier id, CallbackInfoReturnable<UnbakedModel> cir) {
if (fabric_enableGetOrLoadModelGuard && fabric_guardGetOrLoadModel > 0) {
throw new IllegalStateException("ModelLoader#getOrLoadModel called from a ModelResolver or ModelModifier.OnBake instance. This is not allowed to prevent errors during model loading. Use getOrLoadModel from the context instead.");
}
}
@Inject(method = "loadModel", at = @At("HEAD"), cancellable = true)
private void onLoadModel(Identifier id, CallbackInfo ci) {
// Prevent calls to getOrLoadModel from loadModel as it will cause problems.
// Mods should call getOrLoadModel on the ModelResolver.Context instead.
fabric_guardGetOrLoadModel++;
try {
if (fabric_eventDispatcher.loadModel(id)) {
ci.cancel();
@Inject(method = "getOrLoadModel", at = @At("HEAD"), cancellable = true)
private void allowRecursiveLoading(Identifier id, CallbackInfoReturnable<UnbakedModel> cir) {
// If the stack is empty, this is the top-level call, so proceed as normal.
if (!modelLoadingStack.isEmpty()) {
if (unbakedModels.containsKey(id)) {
cir.setReturnValue(unbakedModels.get(id));
} else if (modelLoadingStack.contains(id)) {
throw new IllegalStateException("Circular reference while loading model '" + id + "' (" + modelLoadingStack.stream().map(i -> i + "->").collect(Collectors.joining()) + id + ")");
} else {
UnbakedModel model = loadModel(id);
unbakedModels.put(id, model);
// These will be loaded at the top-level call.
modelsToLoad.addAll(model.getModelDependencies());
cir.setReturnValue(model);
}
} finally {
fabric_guardGetOrLoadModel--;
}
}
@ModifyVariable(method = "method_61076", at = @At("HEAD"), argsOnly = true)
private UnbakedModel onPutModel(UnbakedModel model, ModelIdentifier id) {
fabric_guardGetOrLoadModel++;
// This is the call that needs to be redirected to support ModelResolvers, but it returns a JsonUnbakedModel.
// Redirect it to always return null and handle the logic in a ModifyVariable right after the call.
@Redirect(method = "getOrLoadModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ModelLoader;loadModelFromJson(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/json/JsonUnbakedModel;"))
private JsonUnbakedModel cancelLoadModelFromJson(ModelLoader self, Identifier id) {
return null;
}
@ModifyVariable(method = "getOrLoadModel", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/client/render/model/ModelLoader;loadModelFromJson(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/model/json/JsonUnbakedModel;"))
private UnbakedModel doLoadModel(UnbakedModel model, @Local(ordinal = 1) Identifier id) {
return loadModel(id);
}
@Unique
private UnbakedModel loadModel(Identifier id) {
modelLoadingStack.add(id);
try {
return fabric_eventDispatcher.modifyModelOnLoad(id, model);
UnbakedModel model = fabric_eventDispatcher.resolveModel(id);
if (model == null) {
model = loadModelFromJson(id);
}
return fabric_eventDispatcher.modifyModelOnLoad(model, id, null);
} finally {
fabric_guardGetOrLoadModel--;
modelLoadingStack.removeLast();
}
}
@ModifyVariable(method = "add", at = @At("HEAD"), argsOnly = true)
private UnbakedModel onAdd(UnbakedModel model, ModelIdentifier id) {
if (ModelLoadingConstants.isResourceModelId(id)) {
return model;
}
return fabric_eventDispatcher.modifyModelOnLoad(model, null, id);
}
@WrapOperation(method = "method_61072(Lnet/minecraft/client/render/model/ModelLoader$SpriteGetter;Lnet/minecraft/client/util/ModelIdentifier;Lnet/minecraft/client/render/model/UnbakedModel;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ModelLoader$BakerImpl;bake(Lnet/minecraft/client/render/model/UnbakedModel;Lnet/minecraft/client/render/model/ModelBakeSettings;)Lnet/minecraft/client/render/model/BakedModel;"))
private BakedModel wrapSingleOuterBake(@Coerce Baker baker, UnbakedModel unbakedModel, ModelBakeSettings settings, Operation<BakedModel> operation, ModelLoader.SpriteGetter spriteGetter, ModelIdentifier id) {
if (ModelLoadingConstants.isResourceModelId(id) || id.equals(ModelLoader.MISSING_MODEL_ID)) {
// Call the baker instead of the operation to ensure the baked model is cached and doesn't end up going
// through events twice.
// This ignores the UnbakedModel in modelsToBake but it should be the same as the one in unbakedModels.
return baker.bake(id.id(), settings);
}
Function<SpriteIdentifier, Sprite> textureGetter = ((BakerImplHooks) baker).fabric_getTextureGetter();
unbakedModel = fabric_eventDispatcher.modifyModelBeforeBake(unbakedModel, null, id, textureGetter, settings, baker);
BakedModel model = operation.call(baker, unbakedModel, settings);
return fabric_eventDispatcher.modifyModelAfterBake(model, null, id, unbakedModel, textureGetter, settings, baker);
}
@Override
public ModelLoadingEventDispatcher fabric_getDispatcher() {
return fabric_eventDispatcher;
@ -152,51 +189,16 @@ public abstract class ModelLoaderMixin implements ModelLoaderHooks {
@Override
public UnbakedModel fabric_getMissingModel() {
return unbakedModels.get(MISSING_ID);
return missingModel;
}
/**
* Unlike getOrLoadModel, this method supports nested model loading.
*
* <p>Vanilla does not due to the iteration over modelsToLoad which causes models to be resolved multiple times,
* possibly leading to crashes.
*/
@Override
public UnbakedModel fabric_getOrLoadModel(Identifier id) {
if (this.unbakedModels.containsKey(id)) {
return this.unbakedModels.get(id);
}
if (!modelsToLoad.add(id)) {
throw new IllegalStateException("Circular reference while loading " + id);
}
try {
loadModel(id);
} finally {
modelsToLoad.remove(id);
}
return unbakedModels.get(id);
return getOrLoadModel(id);
}
@Override
public void fabric_putModel(Identifier id, UnbakedModel model) {
putModel(id, model);
}
@Override
public void fabric_putModelDirectly(Identifier id, UnbakedModel model) {
unbakedModels.put(id, model);
}
@Override
public void fabric_queueModelDependencies(UnbakedModel model) {
modelsToLoad.addAll(model.getModelDependencies());
}
@Override
public JsonUnbakedModel fabric_loadModelFromJson(Identifier id) {
return loadModelFromJson(id);
public void fabric_add(ModelIdentifier id, UnbakedModel model) {
add(id, model);
}
}

View file

@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_17",
"client": [
"BakedModelManagerMixin",
"BlockStatesLoaderMixin",
"ModelLoaderMixin",
"ModelLoaderBakerImplMixin"
],

View file

@ -19,9 +19,6 @@
"fabricloader": ">=0.15.11",
"fabric-api-base": "*"
},
"breaks": {
"fabric-models-v0": "<0.4.0"
},
"description": "Provides hooks for model loading.",
"mixins": [
{

View file

@ -47,7 +47,9 @@ import net.fabricmc.fabric.api.resource.ResourceManagerHelper;
public class ModelTestModClient implements ClientModInitializer {
public static final String ID = "fabric-model-loading-api-v1-testmod";
public static final Identifier MODEL_ID = Identifier.of(ID, "half_red_sand");
public static final Identifier HALF_RED_SAND_MODEL_ID = id("half_red_sand");
public static final Identifier GOLD_BLOCK_MODEL_ID = Identifier.ofVanilla("block/gold_block");
public static final Identifier BROWN_GLAZED_TERRACOTTA_MODEL_ID = Identifier.ofVanilla("block/brown_glazed_terracotta");
static class DownQuadRemovingModel extends ForwardingBakedModel {
DownQuadRemovingModel(BakedModel model) {
@ -65,19 +67,23 @@ public class ModelTestModClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ModelLoadingPlugin.register(pluginContext -> {
pluginContext.addModels(MODEL_ID);
pluginContext.addModels(HALF_RED_SAND_MODEL_ID);
// remove bottom face of gold blocks
pluginContext.modifyModelAfterBake().register(ModelModifier.WRAP_PHASE, (model, context) -> {
if (context.id().getPath().equals("block/gold_block")) {
Identifier id = context.resourceId();
if (id != null && id.equals(GOLD_BLOCK_MODEL_ID)) {
return new DownQuadRemovingModel(model);
} else {
return model;
}
return model;
});
// make fences with west: true and everything else false appear to be a missing model visually
ModelIdentifier fenceId = BlockModels.getModelId(Blocks.OAK_FENCE.getDefaultState().with(HorizontalConnectingBlock.WEST, true));
pluginContext.modifyModelOnLoad().register(ModelModifier.OVERRIDE_PHASE, (model, context) -> {
if (fenceId.equals(context.id())) {
ModelIdentifier id = context.topLevelId();
if (id != null && id.equals(fenceId)) {
return context.getOrLoadModel(ModelLoader.MISSING_ID);
}
@ -86,8 +92,10 @@ public class ModelTestModClient implements ClientModInitializer {
// make brown glazed terracotta appear to be a missing model visually, but without affecting the item, by using pre-bake
// using load here would make the item also appear missing
pluginContext.modifyModelBeforeBake().register(ModelModifier.OVERRIDE_PHASE, (model, context) -> {
if (context.id().getPath().equals("block/brown_glazed_terracotta")) {
return context.loader().getOrLoadModel(ModelLoader.MISSING_ID);
Identifier id = context.resourceId();
if (id != null && id.equals(BROWN_GLAZED_TERRACOTTA_MODEL_ID)) {
return context.baker().getOrLoadModel(ModelLoader.MISSING_ID);
}
return model;
@ -119,4 +127,8 @@ public class ModelTestModClient implements ClientModInitializer {
}
});
}
public static Identifier id(String path) {
return Identifier.of(ID, path);
}
}

View file

@ -16,6 +16,8 @@
package net.fabricmc.fabric.test.model.loading;
import static net.fabricmc.fabric.test.model.loading.ModelTestModClient.id;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
@ -32,10 +34,6 @@ import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
public class NestedModelLoadingTest implements ClientModInitializer {
private static final Logger LOGGER = LogUtils.getLogger();
private static Identifier id(String path) {
return Identifier.of("fabric-model-loading-api-v1-testmod", path);
}
private static final Identifier BASE_MODEL = id("nested_base");
private static final Identifier NESTED_MODEL_1 = id("nested_1");
private static final Identifier NESTED_MODEL_2 = id("nested_2");
@ -84,21 +82,22 @@ public class NestedModelLoadingTest implements ClientModInitializer {
pluginContext.modifyModelOnLoad().register((model, context) -> {
UnbakedModel ret = model;
Identifier id = context.resourceId();
if (context.id().equals(NESTED_MODEL_3)) {
Identifier id = context.id();
if (id != null) {
if (id.equals(NESTED_MODEL_3)) {
LOGGER.info(" Nested model 4 started loading");
ret = context.getOrLoadModel(NESTED_MODEL_4);
LOGGER.info(" Nested model 4 finished loading");
LOGGER.info(" Nested model 4 started loading");
ret = context.getOrLoadModel(NESTED_MODEL_4);
LOGGER.info(" Nested model 4 finished loading");
if (!id.equals(context.id())) {
throw new AssertionError("Context object should not have changed.");
if (!id.equals(context.resourceId())) {
throw new AssertionError("Context object should not have changed.");
}
} else if (id.equals(NESTED_MODEL_4)) {
LOGGER.info(" Nested model 5 started loading");
ret = context.getOrLoadModel(NESTED_MODEL_5);
LOGGER.info(" Nested model 5 finished loading");
}
} else if (context.id().equals(NESTED_MODEL_4)) {
LOGGER.info(" Nested model 5 started loading");
ret = context.getOrLoadModel(NESTED_MODEL_5);
LOGGER.info(" Nested model 5 finished loading");
}
return ret;

View file

@ -55,9 +55,18 @@ public class PreparablePluginTest implements ClientModInitializer {
public void onInitializeClient() {
PreparableModelLoadingPlugin.register(PreparablePluginTest::loadModelReplacements, (replacementModels, pluginContext) -> {
pluginContext.modifyModelOnLoad().register((model, ctx) -> {
@Nullable
UnbakedModel replacementModel = replacementModels.get(ctx.id());
return replacementModel == null ? model : replacementModel;
Identifier id = ctx.resourceId();
if (id != null) {
@Nullable
UnbakedModel replacementModel = replacementModels.get(id);
if (replacementModel != null) {
return replacementModel;
}
}
return model;
});
});
}

View file

@ -16,21 +16,18 @@
package net.fabricmc.fabric.test.model.loading;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.model.BakedModel;
import net.minecraft.resource.ResourceManager;
import net.minecraft.resource.SinglePreparationResourceReloader;
import net.minecraft.util.Identifier;
import net.minecraft.util.Unit;
import net.minecraft.util.profiler.Profiler;
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener;
import net.fabricmc.fabric.api.resource.ResourceReloadListenerKeys;
import net.fabricmc.fabric.api.resource.SimpleSynchronousResourceReloadListener;
public class SpecificModelReloadListener extends SinglePreparationResourceReloader<Unit> implements IdentifiableResourceReloadListener {
public class SpecificModelReloadListener implements SimpleSynchronousResourceReloadListener {
public static final SpecificModelReloadListener INSTANCE = new SpecificModelReloadListener();
public static final Identifier ID = Identifier.of(ModelTestModClient.ID, "specific_model");
@ -41,13 +38,8 @@ public class SpecificModelReloadListener extends SinglePreparationResourceReload
}
@Override
protected Unit prepare(ResourceManager manager, Profiler profiler) {
return Unit.INSTANCE;
}
@Override
protected void apply(Unit loader, ResourceManager manager, Profiler profiler) {
specificModel = MinecraftClient.getInstance().getBakedModelManager().getModel(ModelTestModClient.MODEL_ID);
public void reload(ResourceManager manager) {
specificModel = MinecraftClient.getInstance().getBakedModelManager().getModel(ModelTestModClient.HALF_RED_SAND_MODEL_ID);
}
@Override
@ -57,6 +49,6 @@ public class SpecificModelReloadListener extends SinglePreparationResourceReload
@Override
public Collection<Identifier> getFabricDependencies() {
return Arrays.asList(ResourceReloadListenerKeys.MODELS);
return List.of(ResourceReloadListenerKeys.MODELS);
}
}

View file

@ -6,7 +6,7 @@ testDependencies(project, [
':fabric-block-api-v1',
':fabric-block-view-api-v2',
':fabric-blockrenderlayer-v1',
// ':fabric-model-loading-api-v1', TODO 1.21
':fabric-model-loading-api-v1',
':fabric-object-builder-api-v1',
':fabric-renderer-indigo',
':fabric-resource-loader-v0'

View file

@ -16,7 +16,15 @@
package net.fabricmc.fabric.test.renderer.client;
/* TODO 1.21
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import net.minecraft.client.render.model.UnbakedModel;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.client.model.loading.v1.ModelResolver;
import net.fabricmc.fabric.test.renderer.RendererTest;
public class ModelResolverImpl implements ModelResolver {
private static final Set<Identifier> FRAME_MODEL_LOCATIONS = Set.of(
@ -65,4 +73,3 @@ public class ModelResolverImpl implements ModelResolver {
return null;
}
}
*/

View file

@ -20,16 +20,16 @@ import net.minecraft.client.render.RenderLayer;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap;
import net.fabricmc.fabric.api.client.model.loading.v1.ModelLoadingPlugin;
import net.fabricmc.fabric.test.renderer.FrameBlock;
import net.fabricmc.fabric.test.renderer.Registration;
public final class RendererClientTest implements ClientModInitializer {
@Override
public void onInitializeClient() {
// TODO 1.21
// ModelLoadingPlugin.register(pluginContext -> {
// pluginContext.resolveModel().register(new ModelResolverImpl());
// });
ModelLoadingPlugin.register(pluginContext -> {
pluginContext.resolveModel().register(new ModelResolverImpl());
});
for (FrameBlock frameBlock : Registration.FRAME_BLOCKS) {
// We don't specify a material for the frame mesh,

View file

@ -39,7 +39,7 @@ include 'fabric-key-binding-api-v1'
include 'fabric-lifecycle-events-v1'
include 'fabric-loot-api-v2'
include 'fabric-message-api-v1'
//include 'fabric-model-loading-api-v1' TODO 1.21
include 'fabric-model-loading-api-v1'
include 'fabric-networking-api-v1'
include 'fabric-object-builder-api-v1'
include 'fabric-particles-v1'