diff --git a/patches/net/minecraft/client/resources/model/ModelBakery.java.patch b/patches/net/minecraft/client/resources/model/ModelBakery.java.patch index 694309384e..8d9adab369 100644 --- a/patches/net/minecraft/client/resources/model/ModelBakery.java.patch +++ b/patches/net/minecraft/client/resources/model/ModelBakery.java.patch @@ -1,13 +1,15 @@ --- a/net/minecraft/client/resources/model/ModelBakery.java +++ b/net/minecraft/client/resources/model/ModelBakery.java -@@ -46,19 +_,36 @@ +@@ -46,19 +_,40 @@ private final Map clientInfos; final Map unbakedPlainModels; final UnbakedModel missingModel; + private final Map standaloneModels; ++ @org.jetbrains.annotations.Nullable ++ private final net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager pluginManager; + + /** -+ * @deprecated Neo: use {@link #ModelBakery(EntityModelSet, Map, Map, Map, UnbakedModel, Map)} ModelBakery instead} ++ * @deprecated Neo: use {@link #ModelBakery(EntityModelSet, Map, Map, Map, UnbakedModel, Map, net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager)} ModelBakery instead} + */ + @Deprecated + public ModelBakery( @@ -17,7 +19,7 @@ + Map p_388404_, + UnbakedModel p_360944_ + ) { -+ this(p_388903_, p_251087_, p_250416_, p_388404_, p_360944_, Map.of()); ++ this(p_388903_, p_251087_, p_250416_, p_388404_, p_360944_, Map.of(), null); + } public ModelBakery( @@ -27,7 +29,8 @@ Map p_388404_, - UnbakedModel p_360944_ + UnbakedModel p_360944_, -+ Map standaloneModels ++ Map standaloneModels, ++ @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager pluginManager ) { this.entityModelSet = p_388903_; this.unbakedBlockStateModels = p_251087_; @@ -35,9 +38,26 @@ this.unbakedPlainModels = p_388404_; this.missingModel = p_360944_; + this.standaloneModels = standaloneModels; ++ this.pluginManager = pluginManager; } public ModelBakery.BakingResult bakeModels(ModelBakery.TextureGetter p_352431_) { +@@ -68,7 +_,14 @@ + Map map = new HashMap<>(this.unbakedBlockStateModels.size()); + this.unbakedBlockStateModels.forEach((p_386257_, p_386258_) -> { + try { +- BakedModel bakedmodel1 = p_386258_.bake(new ModelBakery.ModelBakerImpl(p_352431_, p_386257_::toString)); ++ ModelBakerImpl baker = new ModelBakerImpl(p_352431_, p_386257_::toString); ++ if (this.pluginManager != null) { ++ p_386258_ = this.pluginManager.modifyBlockModelBeforeBake(p_386257_, p_386258_, baker); ++ } ++ BakedModel bakedmodel1 = p_386258_.bake(baker); ++ if (this.pluginManager != null) { ++ bakedmodel1 = this.pluginManager.modifyBlockModelAfterBake(p_386257_, bakedmodel1, p_386258_, baker); ++ } + map.put(p_386257_, bakedmodel1); + } catch (Exception exception) { + LOGGER.warn("Unable to bake model: '{}': {}", p_386257_, exception); @@ -92,7 +_,18 @@ LOGGER.warn("Unable to bake item model: '{}'", p_390101_, exception); } @@ -82,3 +102,17 @@ } @OnlyIn(Dist.CLIENT) +@@ -142,7 +_,13 @@ + return bakedmodel; + } else { + UnbakedModel unbakedmodel = this.getModel(p_252176_); ++ if (ModelBakery.this.pluginManager != null) { ++ unbakedmodel = ModelBakery.this.pluginManager.modifyModelBeforeBake(p_252176_, unbakedmodel, p_249765_, this); ++ } + BakedModel bakedmodel1 = UnbakedModel.bakeWithTopModelValues(unbakedmodel, this, p_249765_); ++ if (ModelBakery.this.pluginManager != null) { ++ bakedmodel1 = ModelBakery.this.pluginManager.modifyModelAfterBake(p_252176_, bakedmodel1, unbakedmodel, p_249765_, this); ++ } + ModelBakery.this.bakedCache.put(modelbakery$bakedcachekey, bakedmodel1); + return bakedmodel1; + } diff --git a/patches/net/minecraft/client/resources/model/ModelDiscovery.java.patch b/patches/net/minecraft/client/resources/model/ModelDiscovery.java.patch index dba9824817..4608b29e8f 100644 --- a/patches/net/minecraft/client/resources/model/ModelDiscovery.java.patch +++ b/patches/net/minecraft/client/resources/model/ModelDiscovery.java.patch @@ -1,14 +1,27 @@ --- a/net/minecraft/client/resources/model/ModelDiscovery.java +++ b/net/minecraft/client/resources/model/ModelDiscovery.java -@@ -22,6 +_,7 @@ +@@ -22,15 +_,33 @@ final UnbakedModel missingModel; private final List topModels = new ArrayList<>(); private final Map referencedModels = new HashMap<>(); + final Map standaloneModels = new HashMap<>(); ++ @org.jetbrains.annotations.Nullable ++ private final net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager pluginManager; ++ /** ++ * @deprecated Use {@link ModelDiscovery(Map, UnbakedModel, net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager) instead} ++ */ ++ @Deprecated public ModelDiscovery(Map p_360750_, UnbakedModel p_365355_) { ++ this(p_360750_, p_365355_, null); ++ } ++ ++ public ModelDiscovery(Map p_360750_, UnbakedModel p_365355_, @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager pluginManager) { this.inputModels = p_360750_; -@@ -31,6 +_,12 @@ + this.missingModel = p_365355_; + this.referencedModels.put(MissingBlockModel.LOCATION, p_365355_); ++ this.pluginManager = pluginManager; + } public void registerSpecialModels() { this.referencedModels.put(ItemModelGenerator.GENERATED_ITEM_MODEL_ID, new ItemModelGenerator()); @@ -21,3 +34,13 @@ } public void addRoot(ResolvableModel p_388596_) { +@@ -55,6 +_,9 @@ + + private UnbakedModel loadBlockModel(ResourceLocation p_361274_) { + UnbakedModel unbakedmodel = this.inputModels.get(p_361274_); ++ if (this.pluginManager != null) { ++ unbakedmodel = this.pluginManager.modifyModelOnLoad(p_361274_, unbakedmodel); ++ } + if (unbakedmodel == null) { + LOGGER.warn("Missing block model: '{}'", p_361274_); + return this.missingModel; diff --git a/patches/net/minecraft/client/resources/model/ModelManager.java.patch b/patches/net/minecraft/client/resources/model/ModelManager.java.patch index a5710ab8e9..e9d9870b5c 100644 --- a/patches/net/minecraft/client/resources/model/ModelManager.java.patch +++ b/patches/net/minecraft/client/resources/model/ModelManager.java.patch @@ -1,12 +1,13 @@ --- a/net/minecraft/client/resources/model/ModelManager.java +++ b/net/minecraft/client/resources/model/ModelManager.java -@@ -83,11 +_,15 @@ +@@ -83,11 +_,16 @@ private BakedModel missingModel; private ItemModel missingItemModel; private Object2IntMap modelGroups = Object2IntMaps.emptyMap(); + private final java.util.concurrent.atomic.AtomicReference modelBakery = new java.util.concurrent.atomic.AtomicReference<>(null); + private Map bakedStandaloneModels = Map.of(); + private Set reportedMissingItemModels = new java.util.HashSet<>(); ++ private volatile CompletableFuture pluginFuture = CompletableFuture.completedFuture(null); public ModelManager(TextureManager p_119406_, BlockColors p_119407_, int p_119408_) { this.blockColors = p_119407_; @@ -32,7 +33,7 @@ } public ClientItem.Properties getItemProperties(ResourceLocation p_390438_) { -@@ -115,6 +_,7 @@ +@@ -115,15 +_,18 @@ public final CompletableFuture reload( PreparableReloadListener.PreparationBarrier p_249079_, ResourceManager p_251134_, Executor p_250550_, Executor p_249221_ ) { @@ -40,18 +41,41 @@ UnbakedModel unbakedmodel = MissingBlockModel.missingModel(); CompletableFuture completablefuture = CompletableFuture.supplyAsync(EntityModelSet::vanilla, p_250550_); CompletableFuture completablefuture1 = completablefuture.thenApplyAsync(SpecialBlockModelRenderer::vanilla, p_250550_); -@@ -154,8 +_,10 @@ + CompletableFuture> completablefuture2 = loadBlockModels(p_251134_, p_250550_); +- CompletableFuture completablefuture3 = BlockStateModelLoader.loadBlockStates(unbakedmodel, p_251134_, p_250550_); ++ this.pluginFuture = net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager.prepare(p_251134_, p_250550_); ++ CompletableFuture completablefuture3 = BlockStateModelLoader.loadBlockStates(unbakedmodel, p_251134_, p_250550_) ++ .thenCombine(this.pluginFuture, (models, pluginManager) -> pluginManager.modifyBlockModelsOnLoad(models)); + CompletableFuture completablefuture4 = ClientItemInfoLoader.scheduleLoad(p_251134_, p_250550_); + CompletableFuture completablefuture5 = CompletableFuture.allOf(completablefuture2, completablefuture3, completablefuture4) + .thenApplyAsync( +- p_390107_ -> discoverModelDependencies(unbakedmodel, completablefuture2.join(), completablefuture3.join(), completablefuture4.join()), ++ p_390107_ -> discoverModelDependencies(unbakedmodel, completablefuture2.join(), completablefuture3.join(), completablefuture4.join(), this.pluginFuture.join()), + p_250550_ + ); + CompletableFuture> completablefuture6 = completablefuture3.thenApplyAsync( +@@ -154,13 +_,20 @@ completablefuture3.join().plainModels(), completablefuture4.join().contents(), modeldiscovery.getReferencedModels(), - unbakedmodel + unbakedmodel, -+ modeldiscovery.standaloneModels ++ modeldiscovery.standaloneModels, ++ this.pluginFuture.join() ); + this.modelBakery.set(modelbakery); return loadModels(Profiler.get(), map1, modelbakery, object2intmap, completablefuture.join(), completablefuture1.join()); }, p_250550_ + ) + .thenCompose(p_252255_ -> p_252255_.readyForUpload.thenApply(p_251581_ -> (ModelManager.ReloadState)p_252255_)) ++ .thenApplyAsync(state -> { ++ this.pluginFuture = CompletableFuture.completedFuture(null); ++ return state; ++ }) + .thenCompose(p_249079_::wait) + .thenAcceptAsync(p_372566_ -> this.apply(p_372566_, Profiler.get()), p_249221_); + } @@ -169,7 +_,7 @@ return CompletableFuture.>supplyAsync(() -> MODEL_LISTER.listMatchingResources(p_251361_), p_252189_) .thenCompose( @@ -70,6 +94,35 @@ } return pair; +@@ -197,13 +_,27 @@ + ); + } + ++ /** ++ * @deprecated Use {@link #discoverModelDependencies(UnbakedModel, Map, BlockStateModelLoader.LoadedModels, ClientItemInfoLoader.LoadedClientInfos, net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager)} instead ++ */ ++ @Deprecated + private static ModelDiscovery discoverModelDependencies( + UnbakedModel p_360651_, + Map p_363228_, + BlockStateModelLoader.LoadedModels p_361624_, + ClientItemInfoLoader.LoadedClientInfos p_390496_ + ) { +- ModelDiscovery modeldiscovery = new ModelDiscovery(p_363228_, p_360651_); ++ return discoverModelDependencies(p_360651_, p_363228_, p_361624_, p_390496_, null); ++ } ++ ++ private static ModelDiscovery discoverModelDependencies( ++ UnbakedModel p_360651_, ++ Map p_363228_, ++ BlockStateModelLoader.LoadedModels p_361624_, ++ ClientItemInfoLoader.LoadedClientInfos p_390496_, ++ @org.jetbrains.annotations.Nullable net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager pluginManager ++ ) { ++ ModelDiscovery modeldiscovery = new ModelDiscovery(p_363228_, p_360651_, pluginManager); + p_361624_.forResolving().forEach(modeldiscovery::addRoot); + p_390496_.contents().values().forEach(p_390109_ -> modeldiscovery.addRoot(p_390109_.model())); + modeldiscovery.registerSpecialModels(); @@ -261,6 +_,8 @@ p_386267_.stream().sorted().map(p_386265_ -> " " + p_386265_).collect(Collectors.joining("\n")) ) diff --git a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java index 40d8b2c25e..b70c115021 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java @@ -183,6 +183,7 @@ import net.neoforged.neoforge.client.gui.GuiLayerManager; import net.neoforged.neoforge.client.gui.map.MapDecorationRendererManager; import net.neoforged.neoforge.client.model.data.ModelData; +import net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPluginManager; import net.neoforged.neoforge.client.renderstate.RegisterRenderStateModifiersEvent; import net.neoforged.neoforge.common.NeoForge; import net.neoforged.neoforge.common.NeoForgeConfig; @@ -998,6 +999,7 @@ public static void initClientHooks(Minecraft mc, ReloadableResourceManager resou DimensionTransitionScreenManager.init(); AnimationTypeManager.init(); CoreShaderManager.init(); + ModelLoadingPluginManager.init(); } /** diff --git a/src/main/java/net/neoforged/neoforge/client/event/ModelEvent.java b/src/main/java/net/neoforged/neoforge/client/event/ModelEvent.java index a3a234a883..aaed5d42ce 100644 --- a/src/main/java/net/neoforged/neoforge/client/event/ModelEvent.java +++ b/src/main/java/net/neoforged/neoforge/client/event/ModelEvent.java @@ -20,6 +20,10 @@ import net.neoforged.fml.LogicalSide; import net.neoforged.fml.event.IModBusEvent; import net.neoforged.neoforge.client.model.UnbakedModelLoader; +import net.neoforged.neoforge.client.model.loadingplugin.BlockStateResolver; +import net.neoforged.neoforge.client.model.loadingplugin.ModelLoadingPlugin; +import net.neoforged.neoforge.client.model.loadingplugin.ModelModifier; +import net.neoforged.neoforge.client.model.loadingplugin.PreparableModelLoadingPlugin; import org.jetbrains.annotations.ApiStatus; /** @@ -43,7 +47,10 @@ protected ModelEvent() {} *

This event is not {@linkplain ICancellableEvent cancellable}.

* *

This event is fired on the mod-specific event bus, only on the {@linkplain LogicalSide#CLIENT logical client}.

+ * + * @deprecated Use {@link BlockStateResolver}s or {@link ModelModifier}s instead */ + @Deprecated(forRemoval = true, since = "1.21.4") public static class ModifyBakingResult extends ModelEvent implements IModBusEvent { private final ModelBakery.BakingResult bakingResult; private final Function textureGetter; @@ -86,7 +93,7 @@ public ModelBakery getModelBakery() { * Fired when the {@link ModelManager} is notified of the resource manager reloading. * Called after the model registry is set up and cached in the {@link net.minecraft.client.renderer.block.BlockModelShaper}.
* The model registry given by this event is unmodifiable. To modify the model registry, use - * {@link ModelEvent.ModifyBakingResult} instead. + * {@link BlockStateResolver}s or {@link ModelModifier}s instead. * *

This event is not {@linkplain ICancellableEvent cancellable}.

* @@ -182,4 +189,27 @@ public void register(ResourceLocation key, UnbakedModelLoader loader) { loaders.put(key, loader); } } + + public static class RegisterLoadingPlugins extends ModelEvent implements IModBusEvent { + private final Map plugins; + private final Map> preparablePlugins; + + @ApiStatus.Internal + public RegisterLoadingPlugins(Map plugins, Map> preparablePlugins) { + this.plugins = plugins; + this.preparablePlugins = preparablePlugins; + } + + public void registerPlugin(ResourceLocation id, ModelLoadingPlugin plugin) { + if (plugins.putIfAbsent(id, plugin) != null) { + throw new IllegalStateException("Duplicate ModelLoadingPlugin registration with ID " + id); + } + } + + public void registerPlugin(ResourceLocation id, PreparableModelLoadingPlugin plugin) { + if (preparablePlugins.putIfAbsent(id, plugin) != null) { + throw new IllegalStateException("Duplicate PreparableModelLoadingPlugin registration with ID " + id); + } + } + } } diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/BlockStateResolver.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/BlockStateResolver.java new file mode 100644 index 0000000000..ac544bfbc3 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/BlockStateResolver.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.model.loadingplugin; + +import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +public interface BlockStateResolver { + void resolveBlockStates(Context context); + + interface Context { + Block getBlock(); + + @Nullable + UnbakedBlockStateModel getModel(BlockState state); + + void setModel(BlockState state, UnbakedBlockStateModel model); + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPlugin.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPlugin.java new file mode 100644 index 0000000000..e5f93f35b0 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPlugin.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.model.loadingplugin; + +import net.minecraft.world.level.block.Block; + +public interface ModelLoadingPlugin { + void initialize(Context context); + + interface Context { + void registerResolver(Block block, BlockStateResolver resolver); + + default void registerModifier(ModelModifier modifier) { + registerModifier(ModelModifier.Phase.DEFAULT, modifier); + } + + void registerModifier(ModelModifier.Phase phase, ModelModifier modifier); + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPluginManager.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPluginManager.java new file mode 100644 index 0000000000..51659ac0c5 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelLoadingPluginManager.java @@ -0,0 +1,310 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.model.loadingplugin; + +import com.google.common.collect.ImmutableList; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.function.BiConsumer; +import net.minecraft.Util; +import net.minecraft.client.renderer.block.BlockModelShaper; +import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.BlockStateModelLoader; +import net.minecraft.client.resources.model.ModelBaker; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.client.resources.model.ModelState; +import net.minecraft.client.resources.model.UnbakedModel; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.fml.ModLoader; +import net.neoforged.neoforge.client.event.ModelEvent; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +public final class ModelLoadingPluginManager { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final Map PLUGINS = new Object2ReferenceLinkedOpenHashMap<>(); + private static final Map> PREPARABLE_PLUGINS = new Object2ReferenceLinkedOpenHashMap<>(); + + private final Map resolvers = new Reference2ReferenceLinkedOpenHashMap<>(); + private final List> onLoadModifiers = new ArrayList<>(); + private final List> beforeBakeModifiers = new ArrayList<>(); + private final List> afterBakeModifiers = new ArrayList<>(); + private final List> onLoadBlockModifiers = new ArrayList<>(); + private final List> beforeBakeBlockModifiers = new ArrayList<>(); + private final List> afterBakeBlockModifiers = new ArrayList<>(); + + private ModelLoadingPluginManager(List plugins) { + var context = new ModelLoadingPlugin.Context() { + private ResourceLocation pluginId; + + @Override + public void registerResolver(Block block, BlockStateResolver resolver) { + if (resolvers.putIfAbsent(block, resolver) != null) { + throw new IllegalStateException("Duplicate BlockStateResolver registration for block " + block); + } + } + + @Override + public void registerModifier(ModelModifier.Phase phase, ModelModifier modifier) { + switch (modifier) { + case ModelModifier.ModifyOnLoad onLoad -> onLoadModifiers.add(new ModifierEntry<>(pluginId, phase, onLoad)); + case ModelModifier.ModifyBeforeBake beforeBake -> beforeBakeModifiers.add(new ModifierEntry<>(pluginId, phase, beforeBake)); + case ModelModifier.ModifyAfterBake afterBake -> afterBakeModifiers.add(new ModifierEntry<>(pluginId, phase, afterBake)); + case ModelModifier.ModifyBlockOnLoad onLoadBlock -> onLoadBlockModifiers.add(new ModifierEntry<>(pluginId, phase, onLoadBlock)); + case ModelModifier.ModifyBlockBeforeBake beforeBakeBlock -> beforeBakeBlockModifiers.add(new ModifierEntry<>(pluginId, phase, beforeBakeBlock)); + case ModelModifier.ModifyBlockAfterBake afterBakeBlock -> afterBakeBlockModifiers.add(new ModifierEntry<>(pluginId, phase, afterBakeBlock)); + } + } + }; + for (PluginEntry plugin : plugins) { + context.pluginId = plugin.id; + plugin.plugin.initialize(context); + } + + Comparator> comp = Comparator.comparing(ModifierEntry::phase); + this.onLoadModifiers.sort(comp); + this.beforeBakeModifiers.sort(comp); + this.afterBakeModifiers.sort(comp); + this.onLoadBlockModifiers.sort(comp); + this.beforeBakeBlockModifiers.sort(comp); + this.afterBakeBlockModifiers.sort(comp); + } + + @ApiStatus.Internal + public static void init() { + ModLoader.postEvent(new ModelEvent.RegisterLoadingPlugins(PLUGINS, PREPARABLE_PLUGINS)); + } + + public static CompletableFuture prepare(ResourceManager resourceManager, Executor executor) { + List> pluginFutures = new ArrayList<>(); + for (Map.Entry entry : PLUGINS.entrySet()) { + pluginFutures.add(CompletableFuture.completedFuture(new PluginEntry(entry.getKey(), entry.getValue()))); + } + for (Map.Entry> entry : PREPARABLE_PLUGINS.entrySet()) { + pluginFutures.add(preparePlugin(entry.getKey(), entry.getValue(), resourceManager, executor)); + } + return Util.sequence(pluginFutures).thenApplyAsync(ModelLoadingPluginManager::new, executor); + } + + private static CompletableFuture preparePlugin(ResourceLocation pluginId, PreparableModelLoadingPlugin plugin, ResourceManager resourceManager, Executor executor) { + CompletableFuture dataFuture = plugin.load(resourceManager, executor); + return dataFuture.thenApply(data -> new PluginEntry(pluginId, ctx -> plugin.initialize(data, ctx))); + } + + @Nullable + public UnbakedModel modifyModelOnLoad(ResourceLocation id, @Nullable UnbakedModel model) { + if (onLoadModifiers.isEmpty()) return model; + + ModelModifier.ModifyOnLoad.Context context = new ModelModifier.ModifyOnLoad.Context(id); + for (ModifierEntry entry : onLoadModifiers) { + try { + model = entry.modifier.modifyModelOnLoad(model, context); + } catch (Throwable t) { + LOGGER.error("On-load modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + return model; + } + + public UnbakedModel modifyModelBeforeBake(ResourceLocation id, UnbakedModel model, ModelState modelState, ModelBaker baker) { + if (beforeBakeModifiers.isEmpty()) return model; + + ModelModifier.ModifyBeforeBake.Context context = new ModelModifier.ModifyBeforeBake.Context(id, modelState, baker); + for (ModifierEntry entry : beforeBakeModifiers) { + try { + model = entry.modifier.modifyModelBeforeBake(model, context); + } catch (Throwable t) { + LOGGER.error("Before-bake modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + return model; + } + + public BakedModel modifyModelAfterBake(ResourceLocation id, BakedModel model, UnbakedModel sourceModel, ModelState modelState, ModelBaker baker) { + if (afterBakeModifiers.isEmpty()) return model; + + ModelModifier.ModifyAfterBake.Context context = new ModelModifier.ModifyAfterBake.Context(id, sourceModel, modelState, baker); + for (ModifierEntry entry : afterBakeModifiers) { + try { + model = entry.modifier.modifyModelAfterBake(model, context); + } catch (Throwable t) { + LOGGER.error("After-bake modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + return model; + } + + public BlockStateModelLoader.LoadedModels modifyBlockModelsOnLoad(BlockStateModelLoader.LoadedModels models) { + if (resolvers.isEmpty() && onLoadBlockModifiers.isEmpty()) return models; + + Map map = models.models(); + if (!(map instanceof HashMap)) { + map = new HashMap<>(map); + models = new BlockStateModelLoader.LoadedModels(map); + } + + if (!resolvers.isEmpty()) { + resolveBlockStates(map); + } + if (!onLoadBlockModifiers.isEmpty()) { + modifyBlockModelsOnLoad(map); + } + + return models; + } + + private void resolveBlockStates(Map map) { + var context = new BlockStateResolver.Context() { + private final Map models = new Reference2ReferenceOpenHashMap<>(); + private Block block; + + @Override + public Block getBlock() { + return block; + } + + @Override + @Nullable + public UnbakedBlockStateModel getModel(BlockState state) { + ModelResourceLocation id = BlockModelShaper.stateToModelLocation(state); + BlockStateModelLoader.LoadedModel model = map.get(id); + return model != null ? model.model() : null; + } + + @Override + public void setModel(BlockState state, UnbakedBlockStateModel model) { + Objects.requireNonNull(state, "State cannot be null"); + Objects.requireNonNull(model, "Model cannot be null"); + + if (!state.is(this.block)) { + throw new IllegalArgumentException("Attempted to set model for state " + state + " on block " + this.block); + } + + if (this.models.putIfAbsent(state, model) != null) { + throw new IllegalStateException("Duplicate model for state " + state + " on block " + this.block); + } + } + }; + for (Map.Entry entry : resolvers.entrySet()) { + Block block = entry.getKey(); + context.block = block; + + boolean errored = false; + try { + entry.getValue().resolveBlockStates(context); + } catch (Throwable t) { + LOGGER.error("Failed to resolve block state models for block {}. Using missing model for all states.", block, t); + errored = true; + } + + Map resolvedModels = context.models; + if (!errored) { + ResourceLocation blockId = BuiltInRegistries.BLOCK.getResourceKey(block).orElseThrow().location(); + ImmutableList states = block.getStateDefinition().getPossibleStates(); + BiConsumer output = (state, model) -> { + ModelResourceLocation modelId = BlockModelShaper.stateToModelLocation(blockId, state); + map.put(modelId, new BlockStateModelLoader.LoadedModel(state, model)); + }; + + if (resolvedModels.size() == states.size()) { + resolvedModels.forEach(output); + } else { + for (BlockState state : states) { + UnbakedBlockStateModel model = resolvedModels.get(state); + if (model != null) { + output.accept(state, model); + } else { + LOGGER.error("Block state resolver did not provide a model for state {} in block {}. Using missing model.", state, block); + } + } + } + } + resolvedModels.clear(); + } + } + + private void modifyBlockModelsOnLoad(Map map) { + var context = new ModelModifier.ModifyBlockOnLoad.Context() { + private ModelResourceLocation id; + private BlockState state; + + @Override + public ModelResourceLocation id() { + return id; + } + + @Override + public BlockState state() { + return state; + } + }; + map.replaceAll((id, loadedModel) -> { + context.id = id; + context.state = loadedModel.state(); + + UnbakedBlockStateModel model = loadedModel.model(); + for (ModifierEntry entry : onLoadBlockModifiers) { + try { + model = entry.modifier.modifyBlockModelOnLoad(model, context); + } catch (Throwable t) { + LOGGER.error("On-load-block modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + if (loadedModel.model() != model) { + return new BlockStateModelLoader.LoadedModel(loadedModel.state(), model); + } + return loadedModel; + }); + } + + public UnbakedBlockStateModel modifyBlockModelBeforeBake(ModelResourceLocation id, UnbakedBlockStateModel model, ModelBaker baker) { + if (beforeBakeBlockModifiers.isEmpty()) return model; + + ModelModifier.ModifyBlockBeforeBake.Context context = new ModelModifier.ModifyBlockBeforeBake.Context(id, baker); + for (ModifierEntry entry : beforeBakeBlockModifiers) { + try { + model = entry.modifier.modifyBlockModelBeforeBake(model, context); + } catch (Throwable t) { + LOGGER.error("Before-bake-block modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + return model; + } + + public BakedModel modifyBlockModelAfterBake(ModelResourceLocation id, BakedModel model, UnbakedBlockStateModel sourceModel, ModelBaker baker) { + if (afterBakeBlockModifiers.isEmpty()) return model; + + ModelModifier.ModifyBlockAfterBake.Context context = new ModelModifier.ModifyBlockAfterBake.Context(id, sourceModel, baker); + for (ModifierEntry entry : afterBakeBlockModifiers) { + try { + model = entry.modifier.modifyBlockModelAfterBake(model, context); + } catch (Throwable t) { + LOGGER.error("After-bake-block modifier {} from plugin {} threw an exception", entry.modifier, entry.owningPlugin, t); + } + } + return model; + } + + private record PluginEntry(ResourceLocation id, ModelLoadingPlugin plugin) {} + + private record ModifierEntry(ResourceLocation owningPlugin, ModelModifier.Phase phase, T modifier) {} +} diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelModifier.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelModifier.java new file mode 100644 index 0000000000..5361096ab9 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/ModelModifier.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.model.loadingplugin; + +import net.minecraft.client.renderer.block.model.UnbakedBlockStateModel; +import net.minecraft.client.resources.model.BakedModel; +import net.minecraft.client.resources.model.ModelBaker; +import net.minecraft.client.resources.model.ModelResourceLocation; +import net.minecraft.client.resources.model.ModelState; +import net.minecraft.client.resources.model.UnbakedModel; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +public sealed interface ModelModifier { + non-sealed interface ModifyOnLoad extends ModelModifier { + @Nullable + default UnbakedModel modifyModelOnLoad(@Nullable UnbakedModel model, Context context) { + return model; + } + + record Context(ResourceLocation id) {} + } + + non-sealed interface ModifyBeforeBake extends ModelModifier { + default UnbakedModel modifyModelBeforeBake(UnbakedModel model, Context context) { + return model; + } + + record Context(ResourceLocation id, ModelState modelState, ModelBaker baker) {} + } + + non-sealed interface ModifyAfterBake extends ModelModifier { + default BakedModel modifyModelAfterBake(BakedModel model, Context context) { + return model; + } + + record Context(ResourceLocation id, UnbakedModel sourceModel, ModelState modelState, ModelBaker baker) {} + } + + non-sealed interface ModifyBlockOnLoad extends ModelModifier { + default UnbakedBlockStateModel modifyBlockModelOnLoad(UnbakedBlockStateModel model, Context context) { + return model; + } + + interface Context { + ModelResourceLocation id(); + + BlockState state(); + } + } + + non-sealed interface ModifyBlockBeforeBake extends ModelModifier { + default UnbakedBlockStateModel modifyBlockModelBeforeBake(UnbakedBlockStateModel model, Context context) { + return model; + } + + record Context(ModelResourceLocation id, ModelBaker baker) {} + } + + non-sealed interface ModifyBlockAfterBake extends ModelModifier { + default BakedModel modifyBlockModelAfterBake(BakedModel model, Context context) { + return model; + } + + record Context(ModelResourceLocation id, UnbakedBlockStateModel sourceModel, ModelBaker baker) {} + } + + enum Phase { + OVERRIDE, + DEFAULT, + WRAP, + WRAP_LAST, + } +} diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/PreparableModelLoadingPlugin.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/PreparableModelLoadingPlugin.java new file mode 100644 index 0000000000..23cbaeeda1 --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/PreparableModelLoadingPlugin.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.model.loadingplugin; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import net.minecraft.server.packs.resources.ResourceManager; + +public interface PreparableModelLoadingPlugin { + CompletableFuture load(ResourceManager resourceManager, Executor executor); + + void initialize(T data, ModelLoadingPlugin.Context context); +} diff --git a/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/package-info.java b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/package-info.java new file mode 100644 index 0000000000..f09f18901e --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/model/loadingplugin/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +@FieldsAreNonnullByDefault +@MethodsReturnNonnullByDefault +@ParametersAreNonnullByDefault +package net.neoforged.neoforge.client.model.loadingplugin; + +import javax.annotation.ParametersAreNonnullByDefault; +import net.minecraft.FieldsAreNonnullByDefault; +import net.minecraft.MethodsReturnNonnullByDefault; diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/MegaModelTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/MegaModelTest.java index 54fc9d6a20..0254042b3f 100644 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/MegaModelTest.java +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/MegaModelTest.java @@ -43,6 +43,7 @@ import net.neoforged.neoforge.client.model.QuadTransformers; import net.neoforged.neoforge.client.model.data.ModelData; import net.neoforged.neoforge.client.model.data.ModelProperty; +import net.neoforged.neoforge.client.model.loadingplugin.ModelModifier; import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import net.neoforged.neoforge.registries.DeferredBlock; import net.neoforged.neoforge.registries.DeferredHolder; @@ -95,10 +96,19 @@ private void addCreative(BuildCreativeModeTabContentsEvent event) { @EventBusSubscriber(value = Dist.CLIENT, modid = MOD_ID, bus = EventBusSubscriber.Bus.MOD) public static class ClientEvents { + private static final ModelResourceLocation BLOCK_MODEL_LOC = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath(MOD_ID, REG_NAME), ""); + @SubscribeEvent - public static void onModelBakingCompleted(ModelEvent.ModifyBakingResult event) { - var name = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath(MOD_ID, REG_NAME), ""); - event.getBakingResult().blockStateModels().computeIfPresent(name, (n, m) -> new TransformingModelWrapper(m)); + public static void onRegisterModelLoadingPlugins(ModelEvent.RegisterLoadingPlugins event) { + event.registerPlugin(ResourceLocation.fromNamespaceAndPath(MOD_ID, "wrap"), ctx -> ctx.registerModifier(new ModelModifier.ModifyBlockAfterBake() { + @Override + public BakedModel modifyBlockModelAfterBake(BakedModel model, Context context) { + if (context.id().equals(BLOCK_MODEL_LOC)) { + return new TransformingModelWrapper(model); + } + return model; + } + })); } } diff --git a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/TRSRTransformerTest.java b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/TRSRTransformerTest.java index aed6f66b18..e1208f20e4 100644 --- a/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/TRSRTransformerTest.java +++ b/tests/src/main/java/net/neoforged/neoforge/oldtest/client/model/TRSRTransformerTest.java @@ -7,7 +7,6 @@ import com.mojang.math.Transformation; import java.util.List; -import java.util.Map; import net.minecraft.client.renderer.RenderType; import net.minecraft.client.renderer.block.model.BakedQuad; import net.minecraft.client.renderer.block.model.ItemTransforms; @@ -15,6 +14,7 @@ import net.minecraft.client.resources.model.BakedModel; import net.minecraft.client.resources.model.ModelResourceLocation; import net.minecraft.core.Direction; +import net.minecraft.resources.ResourceLocation; import net.minecraft.util.RandomSource; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.CreativeModeTabs; @@ -28,6 +28,7 @@ import net.neoforged.neoforge.client.model.IDynamicBakedModel; import net.neoforged.neoforge.client.model.QuadTransformers; import net.neoforged.neoforge.client.model.data.ModelData; +import net.neoforged.neoforge.client.model.loadingplugin.ModelModifier; import net.neoforged.neoforge.common.util.TransformationHelper; import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent; import net.neoforged.neoforge.registries.DeferredBlock; @@ -49,7 +50,7 @@ public class TRSRTransformerTest { public TRSRTransformerTest(IEventBus modEventBus) { if (FMLEnvironment.dist.isClient()) { - modEventBus.addListener(this::onModelBake); + modEventBus.addListener(this::onRegisterModelLoadingPlugins); } BLOCKS.register(modEventBus); ITEMS.register(modEventBus); @@ -61,16 +62,21 @@ private void addCreative(BuildCreativeModeTabContentsEvent event) { event.accept(TEST_ITEM); } - public void onModelBake(ModelEvent.ModifyBakingResult e) { - Map models = e.getBakingResult().blockStateModels(); - for (ModelResourceLocation id : models.keySet()) { - if (MODID.equals(id.id().getNamespace()) && "test".equals(id.id().getPath())) { - models.put(id, new MyBakedModel(models.get(id))); + private void onRegisterModelLoadingPlugins(ModelEvent.RegisterLoadingPlugins event) { + event.registerPlugin(ResourceLocation.fromNamespaceAndPath(MODID, "wrap"), ctx -> ctx.registerModifier(new ModelModifier.ModifyBlockAfterBake() { + private static final ModelResourceLocation BLOCK_MODEL_LOC = new ModelResourceLocation(TEST_BLOCK.getId(), ""); + + @Override + public BakedModel modifyBlockModelAfterBake(BakedModel model, Context context) { + if (context.id().equals(BLOCK_MODEL_LOC)) { + return new MyBakedModel(model); + } + return model; } - } + })); } - public class MyBakedModel implements IDynamicBakedModel { + private static class MyBakedModel implements IDynamicBakedModel { private final BakedModel base; public MyBakedModel(BakedModel base) {