diff --git a/patches/net/minecraft/client/resources/model/ModelManager.java.patch b/patches/net/minecraft/client/resources/model/ModelManager.java.patch index dd6edb6544..cef8b3695d 100644 --- a/patches/net/minecraft/client/resources/model/ModelManager.java.patch +++ b/patches/net/minecraft/client/resources/model/ModelManager.java.patch @@ -1,6 +1,6 @@ --- a/net/minecraft/client/resources/model/ModelManager.java +++ b/net/minecraft/client/resources/model/ModelManager.java -@@ -63,13 +_,14 @@ +@@ -63,18 +_,20 @@ TextureAtlas.LOCATION_BLOCKS, ResourceLocation.withDefaultNamespace("blocks") ); @@ -16,6 +16,12 @@ public ModelManager(TextureManager p_119406_, BlockColors p_119407_, int p_119408_) { this.blockColors = p_119407_; + this.maxMipmapLevels = p_119408_; + this.blockModelShaper = new BlockModelShaper(this); ++ Map VANILLA_ATLASES = net.neoforged.neoforge.client.ClientHooks.gatherMaterialAtlases(ModelManager.VANILLA_ATLASES); + this.atlases = new AtlasSet(VANILLA_ATLASES, p_119406_); + } + @@ -100,6 +_,7 @@ Executor p_249221_ ) { diff --git a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java index 8bc74f258c..005dd1e25b 100644 --- a/src/main/java/net/neoforged/neoforge/client/ClientHooks.java +++ b/src/main/java/net/neoforged/neoforge/client/ClientHooks.java @@ -158,6 +158,7 @@ import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent; import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.event.RegisterMaterialAtlasesEvent; import net.neoforged.neoforge.client.event.RegisterParticleProvidersEvent; import net.neoforged.neoforge.client.event.RegisterShadersEvent; import net.neoforged.neoforge.client.event.RegisterSpriteSourceTypesEvent; @@ -1120,4 +1121,10 @@ public static RecipeBookType[] getFilteredRecipeBookTypeValues() { } return RECIPE_BOOK_TYPES; } + + public static Map gatherMaterialAtlases(Map vanillaAtlases) { + vanillaAtlases = new HashMap<>(vanillaAtlases); + ModLoader.postEvent(new RegisterMaterialAtlasesEvent(vanillaAtlases)); + return Map.copyOf(vanillaAtlases); + } } diff --git a/src/main/java/net/neoforged/neoforge/client/event/RegisterMaterialAtlasesEvent.java b/src/main/java/net/neoforged/neoforge/client/event/RegisterMaterialAtlasesEvent.java new file mode 100644 index 0000000000..9d6f6c68ce --- /dev/null +++ b/src/main/java/net/neoforged/neoforge/client/event/RegisterMaterialAtlasesEvent.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.client.event; + +import java.util.Map; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.TextureAtlas; +import net.minecraft.client.resources.TextureAtlasHolder; +import net.minecraft.client.resources.model.Material; +import net.minecraft.client.resources.model.ModelManager; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.bus.api.Event; +import net.neoforged.bus.api.ICancellableEvent; +import net.neoforged.fml.LogicalSide; +import net.neoforged.fml.event.IModBusEvent; +import org.jetbrains.annotations.ApiStatus; + +/** + * Fired for registering {@linkplain TextureAtlas texture atlases} that will be used with {@link Material} or + * other systems which retrieve the atlas via {@link Minecraft#getTextureAtlas(ResourceLocation)} or + * {@link ModelManager#getAtlas(ResourceLocation)}. + *

+ * If an atlas is registered via this event, then it must NOT be used through a {@link TextureAtlasHolder}. + *

+ * This event fires during startup when the {@link ModelManager} is constructed. + *

+ * 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}. + */ +public class RegisterMaterialAtlasesEvent extends Event implements IModBusEvent { + private final Map atlases; + + @ApiStatus.Internal + public RegisterMaterialAtlasesEvent(Map atlases) { + this.atlases = atlases; + } + + /** + * Register a texture atlas with the given name and info location + * + * @param atlasLocation The name of the texture atlas + * @param atlasInfoLocation The location of the atlas info JSON relative to the {@code atlases} directory + */ + public void register(ResourceLocation atlasLocation, ResourceLocation atlasInfoLocation) { + ResourceLocation oldAtlasInfoLoc = this.atlases.putIfAbsent(atlasLocation, atlasInfoLocation); + if (oldAtlasInfoLoc != null) { + throw new IllegalStateException(String.format( + "Duplicate registration of atlas: %s (old info: %s, new info: %s)", + atlasLocation, + oldAtlasInfoLoc, + atlasInfoLocation)); + } + } +} diff --git a/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java b/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java new file mode 100644 index 0000000000..11caca9757 --- /dev/null +++ b/tests/src/main/java/net/neoforged/neoforge/debug/client/TextureAtlasTests.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) NeoForged and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.neoforged.neoforge.debug.client; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; +import net.minecraft.client.resources.model.Material; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManagerReloadListener; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; +import net.neoforged.neoforge.client.event.RegisterMaterialAtlasesEvent; +import net.neoforged.testframework.DynamicTest; +import net.neoforged.testframework.annotation.ForEachTest; +import net.neoforged.testframework.annotation.TestHolder; + +@ForEachTest(side = Dist.CLIENT, groups = { "client.texture_atlas", "texture_atlas" }) +public class TextureAtlasTests { + @TestHolder(description = { "Tests that texture atlases intended for use with Material are correctly registered and loaded" }, enabledByDefault = true) + static void testMaterialAtlas(final DynamicTest test) { + String modId = test.createModId(); + ResourceLocation atlasLoc = ResourceLocation.fromNamespaceAndPath(modId, "textures/atlas/material_test.png"); + + test.framework().modEventBus().addListener(RegisterMaterialAtlasesEvent.class, event -> { + ResourceLocation infoLoc = ResourceLocation.fromNamespaceAndPath(modId, "material_test"); + event.register(atlasLoc, infoLoc); + }); + + test.framework().modEventBus().addListener(RegisterClientReloadListenersEvent.class, event -> { + event.registerReloadListener((ResourceManagerReloadListener) manager -> { + try { + Minecraft.getInstance().getModelManager().getAtlas(atlasLoc); + } catch (NullPointerException npe) { + test.fail("Atlas was not registered"); + return; + } catch (Throwable t) { + test.fail("Atlas lookup failed: " + t.getMessage()); + return; + } + + try { + Material material = new Material(atlasLoc, ResourceLocation.withDefaultNamespace("block/stone")); + TextureAtlasSprite sprite = material.sprite(); + if (sprite.contents().name().equals(MissingTextureAtlasSprite.getLocation())) { + test.fail("Expected sprite was not stitched"); + return; + } + } catch (Throwable t) { + test.fail("Sprite lookup via material failed: " + t.getMessage()); + } + + test.pass(); + }); + }); + } +} diff --git a/tests/src/main/resources/assets/neotests_test_material_atlas/atlases/material_test.json b/tests/src/main/resources/assets/neotests_test_material_atlas/atlases/material_test.json new file mode 100644 index 0000000000..fcad3a1669 --- /dev/null +++ b/tests/src/main/resources/assets/neotests_test_material_atlas/atlases/material_test.json @@ -0,0 +1,8 @@ +{ + "sources": [ + { + "type": "minecraft:single", + "resource": "minecraft:block/stone" + } + ] +}