From ee28693d3c830b6f7060ee8a939536b4555e4813 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 9 Jul 2026 02:17:47 -0500 Subject: [PATCH 01/18] Fix variant block base texture never rendering in world --- .../com/ruling_0/materiallib/api/ShapeBlock.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 6f72edc4..cd42cd48 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -15,7 +15,7 @@ import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; -import net.minecraftforge.client.MinecraftForgeClient; +import net.minecraftforge.client.ForgeHooksClient; import cpw.mods.fml.common.registry.GameRegistry; import cpw.mods.fml.relauncher.Side; @@ -178,8 +178,10 @@ protected String iconPathFor(Material material) { /// A block with no base texture renders as a single tinted layer. A block with a base texture /// renders in two passes -- the untinted base in the solid pass 0, and the tinted material icon over it in /// the alpha-blended pass 1 (pass 1 draws after pass 0, and the material texture's transparent pixels let - /// the base show through). The item form never sets an active render pass (pass -1) and shows the tinted - /// material icon; see [#canRenderInPass]. + /// the base show through). [#getIcon] and [#colorMultiplier] tell the two passes apart through + /// [ForgeHooksClient#getWorldRenderPass], which [ForgeHooksClient] only sets to 0 or 1 around world chunk + /// tessellation; it is -1 everywhere else, including the item form, which always shows the tinted material + /// icon; see [#canRenderInPass]. @Override @SideOnly(Side.CLIENT) public int getRenderBlockPass() { return baseTexture != null ? 1 : 0; } @@ -193,7 +195,7 @@ public boolean canRenderInPass(int pass) { @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { - if (baseTexture != null && MinecraftForgeClient.getRenderPass() == 0) { + if (baseTexture != null && ForgeHooksClient.getWorldRenderPass() == 0) { return baseIcon; } return icons.get(meta); @@ -208,7 +210,7 @@ public int getRenderColor(int meta) { @Override @SideOnly(Side.CLIENT) public int colorMultiplier(IBlockAccess world, int x, int y, int z) { - if (baseTexture != null && MinecraftForgeClient.getRenderPass() == 0) { + if (baseTexture != null && ForgeHooksClient.getWorldRenderPass() == 0) { return 0xFFFFFF; } return tintFor(world.getBlockMetadata(x, y, z)); From 1b56fb60ce6b88622279bde0515684a7f85d3143 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:23:03 -0500 Subject: [PATCH 02/18] Fix variant ore block underlay missing in item form --- .../com/ruling_0/materiallib/ClientProxy.java | 10 +++ .../ruling_0/materiallib/api/ShapeBlock.java | 59 +++++++++++++-- .../api/ShapeBlockItemRenderer.java | 72 +++++++++++++++++++ .../materiallib/api/ShapeBlockTest.java | 51 +++++++++++++ 4 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java create mode 100644 src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java diff --git a/src/main/java/com/ruling_0/materiallib/ClientProxy.java b/src/main/java/com/ruling_0/materiallib/ClientProxy.java index 0e88e683..c454ecee 100644 --- a/src/main/java/com/ruling_0/materiallib/ClientProxy.java +++ b/src/main/java/com/ruling_0/materiallib/ClientProxy.java @@ -1,8 +1,12 @@ package com.ruling_0.materiallib; +import net.minecraft.item.Item; + import net.minecraftforge.client.MinecraftForgeClient; import net.minecraftforge.common.MinecraftForge; +import com.ruling_0.materiallib.api.ShapeBlock; +import com.ruling_0.materiallib.api.ShapeBlockItemRenderer; import com.ruling_0.materiallib.api.ShapeFluidIcons; import com.ruling_0.materiallib.api.ShapeItem; import com.ruling_0.materiallib.api.ShapeItemRenderer; @@ -19,6 +23,12 @@ public void preInit(FMLPreInitializationEvent event) { for (ShapeItem item : ShapeRegistry.instance().getItemShapes()) { MinecraftForgeClient.registerItemRenderer(item, renderer); } + for (ShapeBlock block : ShapeRegistry.instance().getBlockShapes()) { + if (block.hasBaseTexture()) { + MinecraftForgeClient.registerItemRenderer(Item.getItemFromBlock(block), + new ShapeBlockItemRenderer(block)); + } + } MinecraftForge.EVENT_BUS.register(new ShapeFluidIcons()); } } diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index cd42cd48..5293ab6b 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -17,6 +17,9 @@ import net.minecraftforge.client.ForgeHooksClient; +import com.gtnewhorizon.gtnhlib.util.ResourceUtil; +import com.ruling_0.materiallib.MaterialLib; + import cpw.mods.fml.common.registry.GameRegistry; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -54,6 +57,8 @@ public class ShapeBlock extends Block implements BackedShape { private final ServedMaterials served = new ServedMaterials(); private final ShapeIcons icons = new ShapeIcons(false); private IIcon baseIcon; + private boolean warnedMissingBaseTexture; + private int itemRenderPass = -1; /// Creates a block shape backed by a [net.minecraft.block.material.Material#iron] block. `oreDicts` are the /// oredict prefixes, at least one; `displayNameFormat` is applied to the material name to build the display @@ -164,10 +169,44 @@ public void registerBlockIcons(IIconRegister register) { List candidates = groupName != null ? List.of(name, groupName) : List.of(name); icons.bind(register, served.get(), candidates, this::iconPathFor); if (baseTexture != null) { - baseIcon = register.registerIcon(baseTexture); + baseIcon = registerBaseIcon(register); + } + } + + /// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder -- logged + /// once -- if it does not, the same existence-checked fallback [ShapeIcons] uses for a material's texture-set + /// icon. Without this check a bad base texture path renders as Minecraft's own unlogged missing-texture + /// checkerboard instead of a diagnosable warning. + private IIcon registerBaseIcon(IIconRegister register) { + if (ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(baseTexture))) { + return register.registerIcon(baseTexture); } + if (!warnedMissingBaseTexture) { + warnedMissingBaseTexture = true; + MaterialLib.LOG.warn( + "Block shape {} variant {} has no base texture at {}; it will render the empty placeholder instead", + name, + variant, + baseTexture); + } + return register.registerIcon(ShapeIcons.EMPTY_ICON); + } + + /// Whether this variant draws a base texture layer under the tinted material icon; see [#registerBlockIcons]. + /// [ClientProxy][com.ruling_0.materiallib.ClientProxy] uses this to decide which block shapes need + /// [ShapeBlockItemRenderer] -- a plain block shape has nothing to composite and keeps the vanilla single-pass + /// item renderer. + public boolean hasBaseTexture() { + return baseTexture != null; } + /// Sets the pass [#getIcon] and [#getRenderColor] fall back to when [ForgeHooksClient#getWorldRenderPass] is + /// -1 (i.e. outside world chunk tessellation), or -1 to clear it. [ShapeBlockItemRenderer] toggles this around + /// each of its two [net.minecraft.client.renderer.RenderBlocks#renderBlockAsItem] calls so the item form's + /// base and overlay layers resolve the same icon and color [#getIcon]/[#getRenderColor] give the corresponding + /// world render pass. + void setItemRenderPass(int pass) { itemRenderPass = pass; } + /// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight /// to them. The default implementation defers to this block's [BlockIconPather]. A subclass may override this /// directly. @@ -180,8 +219,8 @@ protected String iconPathFor(Material material) { /// the alpha-blended pass 1 (pass 1 draws after pass 0, and the material texture's transparent pixels let /// the base show through). [#getIcon] and [#colorMultiplier] tell the two passes apart through /// [ForgeHooksClient#getWorldRenderPass], which [ForgeHooksClient] only sets to 0 or 1 around world chunk - /// tessellation; it is -1 everywhere else, including the item form, which always shows the tinted material - /// icon; see [#canRenderInPass]. + /// tessellation; it is -1 everywhere else, including the item form, which falls back to [#itemRenderPass] + /// instead (see [#getIcon], [#getRenderColor], [#setItemRenderPass]); see [#canRenderInPass]. @Override @SideOnly(Side.CLIENT) public int getRenderBlockPass() { return baseTexture != null ? 1 : 0; } @@ -192,10 +231,19 @@ public boolean canRenderInPass(int pass) { return baseTexture == null ? pass == 0 : pass == 0 || pass == 1; } + /// The world-tessellation render pass ([ForgeHooksClient#getWorldRenderPass]) when it is active (0 or 1), or + /// [#itemRenderPass] otherwise -- 0 or 1 while [ShapeBlockItemRenderer] drives the item form's two-pass + /// composite, -1 (falling through to the single tinted layer, [#icons]) everywhere else, matching this + /// block's appearance before [ShapeBlockItemRenderer] existed. + private int renderPass() { + int worldPass = ForgeHooksClient.getWorldRenderPass(); + return worldPass != -1 ? worldPass : itemRenderPass; + } + @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { - if (baseTexture != null && ForgeHooksClient.getWorldRenderPass() == 0) { + if (baseTexture != null && renderPass() == 0) { return baseIcon; } return icons.get(meta); @@ -204,6 +252,9 @@ public IIcon getIcon(int side, int meta) { @Override @SideOnly(Side.CLIENT) public int getRenderColor(int meta) { + if (baseTexture != null && itemRenderPass == 0) { + return 0xFFFFFF; + } return tintFor(meta); } diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java new file mode 100644 index 00000000..a422145d --- /dev/null +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java @@ -0,0 +1,72 @@ +package com.ruling_0.materiallib.api; + +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.item.ItemStack; + +import net.minecraftforge.client.IItemRenderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.lwjgl.opengl.GL11; + +/// Renders a variant block's item form -- a normal full cube -- with the same base-and-overlay composite as +/// [ShapeBlock]'s world rendering. +/// +/// Vanilla's block-as-item renderer ([RenderBlocks#renderBlockAsItem]) draws exactly one icon per face, driven by +/// a single call to [net.minecraft.block.Block#getRenderColor], so it cannot reproduce [ShapeBlock#getIcon]'s +/// pass-conditioned base/overlay split by itself; unlike world chunk tessellation, it never sets +/// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], so [ShapeBlock#getIcon] always resolves the +/// tinted material icon and the untinted base never shows. This renderer calls `renderBlockAsItem` twice instead, +/// toggling [ShapeBlock#setItemRenderPass] between calls so [ShapeBlock#getIcon] and [ShapeBlock#getRenderColor] +/// pick the same base/overlay layer each pass that world tessellation would. +/// +/// [com.ruling_0.materiallib.ClientProxy] registers one instance per variant block whose shape has a base texture +/// ([ShapeBlock#hasBaseTexture]); a plain block shape has nothing to composite and keeps the vanilla single-pass +/// item renderer. +@SideOnly(Side.CLIENT) +public final class ShapeBlockItemRenderer implements IItemRenderer { + + private final ShapeBlock shape; + + public ShapeBlockItemRenderer(ShapeBlock shape) { + this.shape = shape; + } + + @Override + public boolean handleRenderType(ItemStack item, ItemRenderType type) { + return true; + } + + @Override + public boolean shouldUseRenderHelper(ItemRenderType type, ItemStack item, ItemRendererHelper helper) { + return true; + } + + @Override + public void renderItem(ItemRenderType type, ItemStack item, Object... data) { + RenderBlocks renderBlocks = data.length > 0 && data[0] instanceof RenderBlocks rb ? rb : new RenderBlocks(); + int meta = item.getItemDamage(); + boolean wasInventoryTint = renderBlocks.useInventoryTint; + renderBlocks.useInventoryTint = true; + + try { + shape.setItemRenderPass(0); + GL11.glDisable(GL11.GL_BLEND); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); + renderBlocks.renderBlockAsItem(shape, meta, 1.0F); + + shape.setItemRenderPass(1); + GL11.glEnable(GL11.GL_BLEND); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.1F); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + renderBlocks.renderBlockAsItem(shape, meta, 1.0F); + } + finally { + shape.setItemRenderPass(-1); + renderBlocks.useInventoryTint = wasInventoryTint; + GL11.glDisable(GL11.GL_BLEND); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); + } + } +} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java new file mode 100644 index 00000000..b3b91d8e --- /dev/null +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -0,0 +1,51 @@ +package com.ruling_0.materiallib.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/// Headless coverage for [ShapeBlock]'s base-texture bookkeeping. [ShapeBlock#getIcon] and [ShapeBlock#colorMultiplier] +/// additionally consult [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], which only behaves as +/// documented around real world chunk tessellation, and [ShapeBlock#getRenderColor]'s non-white branch resolves a +/// material through the process-wide [MaterialRegistry#instance], which no test resolves; neither is exercised +/// here. Both need a live client (see the in-game example content) -- the user must verify in-client that a variant +/// ore block shows its base texture under the tinted material icon both placed in world and in inventory/held/ +/// dropped item form. +class ShapeBlockTest { + + private final ShapeBlock withBaseTexture = new ShapeBlock( + "testmod", + "ore_stone", + "%s Ore", + new String[] { "ore" }, + "ore", + "stone", + "minecraft:stone", + BlockBehavior.NONE, + null); + + private final ShapeBlock withoutBaseTexture = new ShapeBlock("testmod", "ingot", "%s Ingot", "ingot"); + + @Test + void hasBaseTextureReflectsWhetherAVariantBaseWasDeclared() { + assertTrue(withBaseTexture.hasBaseTexture()); + assertFalse(withoutBaseTexture.hasBaseTexture()); + } + + /// Pass 0 of the item-form composite ([ShapeBlockItemRenderer]) must draw the base texture untinted, the same + /// as pass 0 of the world composite ([ShapeBlock#colorMultiplier]) does. This branch returns before consulting + /// any material, so it holds regardless of the given metadata, including one bound to no live material. + @Test + void itemRenderPassZeroIsUntintedWhenABaseTextureExists() { + withBaseTexture.setItemRenderPass(0); + try { + assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(0)); + assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(999)); + } + finally { + withBaseTexture.setItemRenderPass(-1); + } + } +} From 75cbfc647b47b034a6b2430d019e8dd43be8a82d Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:23:22 -0500 Subject: [PATCH 03/18] Fix ore block underlay showing transparent instead of stone in world, inventory, and hotbar --- .../ruling_0/materiallib/api/ShapeBlock.java | 41 ++++++++++++------- .../api/ShapeBlockItemRenderer.java | 22 +++++++++- .../api/ResourceUtilBaseTexturePathTest.java | 40 ++++++++++++++++++ .../materiallib/api/ShapeBlockTest.java | 39 ++++++++++++++---- 4 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 5293ab6b..e7c8709c 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -176,7 +176,10 @@ public void registerBlockIcons(IIconRegister register) { /// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder -- logged /// once -- if it does not, the same existence-checked fallback [ShapeIcons] uses for a material's texture-set /// icon. Without this check a bad base texture path renders as Minecraft's own unlogged missing-texture - /// checkerboard instead of a diagnosable warning. + /// checkerboard instead of a diagnosable warning. The existence check and the registration both resolve + /// [#baseTexture] itself -- never a derived or re-formatted copy of it -- so they always agree; see + /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation] for how that single + /// string maps to the file the check looks for. private IIcon registerBaseIcon(IIconRegister register) { if (ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(baseTexture))) { return register.registerIcon(baseTexture); @@ -200,11 +203,11 @@ public boolean hasBaseTexture() { return baseTexture != null; } - /// Sets the pass [#getIcon] and [#getRenderColor] fall back to when [ForgeHooksClient#getWorldRenderPass] is - /// -1 (i.e. outside world chunk tessellation), or -1 to clear it. [ShapeBlockItemRenderer] toggles this around - /// each of its two [net.minecraft.client.renderer.RenderBlocks#renderBlockAsItem] calls so the item form's - /// base and overlay layers resolve the same icon and color [#getIcon]/[#getRenderColor] give the corresponding - /// world render pass. + /// Sets the pass [#renderPass] falls back to when [ForgeHooksClient#getWorldRenderPass] is -1 (i.e. outside + /// world chunk tessellation), or -1 to clear it. [ShapeBlockItemRenderer] toggles this around each of its two + /// [net.minecraft.client.renderer.RenderBlocks#renderBlockAsItem] calls so the item form's base and overlay + /// layers resolve the same icon and color [#getIcon]/[#getRenderColor] give the corresponding world render + /// pass; see [#renderPass] for what -1 resolves to everywhere else. void setItemRenderPass(int pass) { itemRenderPass = pass; } /// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight @@ -231,19 +234,29 @@ public boolean canRenderInPass(int pass) { return baseTexture == null ? pass == 0 : pass == 0 || pass == 1; } - /// The world-tessellation render pass ([ForgeHooksClient#getWorldRenderPass]) when it is active (0 or 1), or - /// [#itemRenderPass] otherwise -- 0 or 1 while [ShapeBlockItemRenderer] drives the item form's two-pass - /// composite, -1 (falling through to the single tinted layer, [#icons]) everywhere else, matching this - /// block's appearance before [ShapeBlockItemRenderer] existed. + /// [#itemRenderPass] when [ShapeBlockItemRenderer] has set it (0 or 1, driving the item form's two-pass + /// composite), or the world-tessellation render pass ([ForgeHooksClient#getWorldRenderPass]) otherwise -- 0 + /// or 1 during world chunk tessellation, -1 everywhere else. [ShapeBlockItemRenderer] and world tessellation + /// never run nested inside one another, so which of the two this checks first never changes the result; item + /// pass first also means a call with [#itemRenderPass] set never touches [ForgeHooksClient], which needs a + /// live client and is otherwise unreachable from a headless test (see [ShapeBlockTest]'s javadoc). + /// + /// A -1 result (the common "everywhere else" case) resolves to the base layer the same as pass 0 does (see + /// [#getIcon], [#getRenderColor]), not the tinted overlay: vanilla's inventory and hotbar slot icon + /// ([net.minecraft.client.renderer.entity.RenderItem#renderItemIntoGUI]) draws a full-cube block item by + /// calling [#getIcon]/[#getRenderColor] directly, once, with no Forge extension point a mod can hook to run a + /// second, overlay pass -- the untinted base is still a recognizable icon there, where the tinted overlay + /// alone (this class's behavior before this fallback existed) rendered as a transparent slot with a few + /// floating tinted flecks, since the overlay icon is a sparse, mostly-transparent layer meant to be drawn + /// over the base, never standalone. private int renderPass() { - int worldPass = ForgeHooksClient.getWorldRenderPass(); - return worldPass != -1 ? worldPass : itemRenderPass; + return itemRenderPass != -1 ? itemRenderPass : ForgeHooksClient.getWorldRenderPass(); } @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { - if (baseTexture != null && renderPass() == 0) { + if (baseTexture != null && renderPass() != 1) { return baseIcon; } return icons.get(meta); @@ -252,7 +265,7 @@ public IIcon getIcon(int side, int meta) { @Override @SideOnly(Side.CLIENT) public int getRenderColor(int meta) { - if (baseTexture != null && itemRenderPass == 0) { + if (baseTexture != null && renderPass() != 1) { return 0xFFFFFF; } return tintFor(meta); diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java index a422145d..5ec5c53b 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java @@ -11,7 +11,10 @@ import org.lwjgl.opengl.GL11; /// Renders a variant block's item form -- a normal full cube -- with the same base-and-overlay composite as -/// [ShapeBlock]'s world rendering. +/// [ShapeBlock]'s world rendering, for every render type Forge dispatches to a custom [IItemRenderer]: a dropped +/// item entity, and the item equipped in a player's hand (third and first person). Vanilla's own inventory and +/// hotbar slot icon never reaches this class at all -- see [ShapeBlock#renderPass]'s javadoc for that path and +/// how [ShapeBlock] degrades gracefully there instead. /// /// Vanilla's block-as-item renderer ([RenderBlocks#renderBlockAsItem]) draws exactly one icon per face, driven by /// a single call to [net.minecraft.block.Block#getRenderColor], so it cannot reproduce [ShapeBlock#getIcon]'s @@ -21,12 +24,22 @@ /// toggling [ShapeBlock#setItemRenderPass] between calls so [ShapeBlock#getIcon] and [ShapeBlock#getRenderColor] /// pick the same base/overlay layer each pass that world tessellation would. /// +/// The two passes draw the same full cube geometry at the same depth, which -- unlike world chunk tessellation's +/// single tessellated pass -- z-fights under the default depth test: two independent draw calls' fragments land +/// at the same depth up to floating-point rounding, so the depth test can reject either one unpredictably. The +/// overlay pass is drawn with [org.lwjgl.opengl.GL11#GL_LEQUAL] depth testing (so an equal-depth fragment always +/// wins over the base pass already in the depth buffer) and scaled up by [#OVERLAY_SCALE] around the cube's +/// center (so it wins even under stricter depth tests some other mod's GL state might have left active), both +/// restored once the composite is done. +/// /// [com.ruling_0.materiallib.ClientProxy] registers one instance per variant block whose shape has a base texture /// ([ShapeBlock#hasBaseTexture]); a plain block shape has nothing to composite and keeps the vanilla single-pass /// item renderer. @SideOnly(Side.CLIENT) public final class ShapeBlockItemRenderer implements IItemRenderer { + private static final float OVERLAY_SCALE = 1.002F; + private final ShapeBlock shape; public ShapeBlockItemRenderer(ShapeBlock shape) { @@ -60,13 +73,20 @@ public void renderItem(ItemRenderType type, ItemStack item, Object... data) { GL11.glEnable(GL11.GL_BLEND); GL11.glAlphaFunc(GL11.GL_GREATER, 0.1F); OpenGlHelper.glBlendFunc(770, 771, 1, 0); + GL11.glDepthFunc(GL11.GL_LEQUAL); + GL11.glPushMatrix(); + GL11.glTranslatef(0.5F, 0.5F, 0.5F); + GL11.glScalef(OVERLAY_SCALE, OVERLAY_SCALE, OVERLAY_SCALE); + GL11.glTranslatef(-0.5F, -0.5F, -0.5F); renderBlocks.renderBlockAsItem(shape, meta, 1.0F); + GL11.glPopMatrix(); } finally { shape.setItemRenderPass(-1); renderBlocks.useInventoryTint = wasInventoryTint; GL11.glDisable(GL11.GL_BLEND); GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); + GL11.glDepthFunc(GL11.GL_LEQUAL); } } } diff --git a/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java b/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java new file mode 100644 index 00000000..624aa561 --- /dev/null +++ b/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java @@ -0,0 +1,40 @@ +package com.ruling_0.materiallib.api; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.minecraft.util.ResourceLocation; + +import com.gtnewhorizon.gtnhlib.util.ResourceUtil; + +import org.junit.jupiter.api.Test; + +/// Pins [ResourceUtil]'s block-texture path construction, the same construction +/// [ShapeBlock#registerBaseIcon] uses both to existence-check a variant's base texture and, on success, to +/// register it -- independent of any live Minecraft resource manager (see [ShapeBlockTest]'s javadoc for why the +/// existence check itself still needs a live client). A wrong construction here would make a real texture +/// (`minecraft:stone`) register as if it were missing, or send a mixed-case cross-mod domain +/// (`GalacticraftCore:moon`) looking under the wrong domain; 1.7.10 resource lookups are case-sensitive against +/// the packed jar entries a resource pack actually declares. +class ResourceUtilBaseTexturePathTest { + + @Test + void vanillaDomainedPathResolvesUnderTheBlockAtlasConvention() { + ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("minecraft:stone"); + assertEquals("minecraft", location.getResourceDomain()); + assertEquals("textures/blocks/stone.png", location.getResourcePath()); + } + + @Test + void mixedCaseDomainIsPreservedRatherThanLowercased() { + ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("GalacticraftCore:moon"); + assertEquals("GalacticraftCore", location.getResourceDomain()); + assertEquals("textures/blocks/moon.png", location.getResourcePath()); + } + + @Test + void domainlessPathDefaultsToMinecraft() { + ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("stone"); + assertEquals("minecraft", location.getResourceDomain()); + assertEquals("textures/blocks/stone.png", location.getResourcePath()); + } +} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index b3b91d8e..421ff922 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -1,18 +1,25 @@ package com.ruling_0.materiallib.api; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -/// Headless coverage for [ShapeBlock]'s base-texture bookkeeping. [ShapeBlock#getIcon] and [ShapeBlock#colorMultiplier] -/// additionally consult [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], which only behaves as -/// documented around real world chunk tessellation, and [ShapeBlock#getRenderColor]'s non-white branch resolves a -/// material through the process-wide [MaterialRegistry#instance], which no test resolves; neither is exercised -/// here. Both need a live client (see the in-game example content) -- the user must verify in-client that a variant -/// ore block shows its base texture under the tinted material icon both placed in world and in inventory/held/ -/// dropped item form. +/// Headless coverage for [ShapeBlock]'s base-texture bookkeeping. [ShapeBlock#renderPass] checks +/// [ShapeBlock#itemRenderPass] before falling back to +/// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], which needs a live client and is unreachable +/// from this suite (calling it throws `NoClassDefFoundError: org/lwjgl/LWJGLException`, since LWJGL is not on the +/// test runtime classpath); every case here sets [ShapeBlock#itemRenderPass] explicitly so that fallback is never +/// reached. [ShapeBlock#getRenderColor]'s tinted branch resolves a material through the process-wide +/// [MaterialRegistry#instance], which is unresolved (and throws on lookup) until MaterialLib's preInit runs, so +/// no test here can reach it at all; [ShapeBlock#getIcon] never consults a material and stands in for it where +/// only the pass selection, not the tint value, is under test. The -1 (unset) default and world-tessellation +/// behavior both need a live client -- +/// the user must verify in-client that a variant ore block shows its base texture under the tinted material icon +/// placed in world, in inventory/hotbar (base only -- see [ShapeBlock#renderPass]'s javadoc), and held/dropped +/// (full composite, via [ShapeBlockItemRenderer]). class ShapeBlockTest { private final ShapeBlock withBaseTexture = new ShapeBlock( @@ -48,4 +55,22 @@ void itemRenderPassZeroIsUntintedWhenABaseTextureExists() { withBaseTexture.setItemRenderPass(-1); } } + + /// Pass 1 is the overlay pass; [ShapeBlock#renderPass] must resolve it from [ShapeBlock#itemRenderPass] alone, + /// the same as pass 0, never falling through to + /// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass] (which would throw here; see this class's + /// javadoc) -- a future change that reordered those two checks would still pass + /// [#itemRenderPassZeroIsUntintedWhenABaseTextureExists] (pass 0 short-circuits either order) but fail here. + /// [ShapeBlock#getIcon] never consults a material, unlike [ShapeBlock#getRenderColor]'s tinted branch, so it + /// is the one exercised here (see this class's javadoc on why the tinted branch cannot be). + @Test + void itemRenderPassOneNeverTouchesTheWorldRenderPass() { + withBaseTexture.setItemRenderPass(1); + try { + assertDoesNotThrow(() -> withBaseTexture.getIcon(0, 0)); + } + finally { + withBaseTexture.setItemRenderPass(-1); + } + } } From 42b0eceeec64b540c0ce6c27e002f31ef2980da1 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 9 Jul 2026 23:48:35 -0500 Subject: [PATCH 04/18] Confirm ShapeBlockItemRenderer already composites GUI slot and hotbar icons --- .../ruling_0/materiallib/api/ShapeBlock.java | 20 ++++---- .../api/ShapeBlockItemRenderer.java | 33 +++++++++---- .../api/ShapeBlockItemRendererTest.java | 47 +++++++++++++++++++ .../materiallib/api/ShapeBlockTest.java | 4 +- 4 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index e7c8709c..86b97e91 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -39,6 +39,8 @@ /// A variant block built by [ShapeBlockVariants] additionally falls back from its own icon (`_`) /// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) in the solid render /// pass, under the tinted material icon drawn in the alpha pass; see [#registerBlockIcons] and [#canRenderInPass]. +/// The same base-and-overlay composite is reproduced for the item form -- GUI slot, hotbar, held, and dropped -- +/// by [ShapeBlockItemRenderer]; see [#renderPass] for how the two mechanisms share [#getIcon] and [#getRenderColor]. /// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest /// tool class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla /// default it replaces. @@ -241,14 +243,16 @@ public boolean canRenderInPass(int pass) { /// pass first also means a call with [#itemRenderPass] set never touches [ForgeHooksClient], which needs a /// live client and is otherwise unreachable from a headless test (see [ShapeBlockTest]'s javadoc). /// - /// A -1 result (the common "everywhere else" case) resolves to the base layer the same as pass 0 does (see - /// [#getIcon], [#getRenderColor]), not the tinted overlay: vanilla's inventory and hotbar slot icon - /// ([net.minecraft.client.renderer.entity.RenderItem#renderItemIntoGUI]) draws a full-cube block item by - /// calling [#getIcon]/[#getRenderColor] directly, once, with no Forge extension point a mod can hook to run a - /// second, overlay pass -- the untinted base is still a recognizable icon there, where the tinted overlay - /// alone (this class's behavior before this fallback existed) rendered as a transparent slot with a few - /// floating tinted flecks, since the overlay icon is a sparse, mostly-transparent layer meant to be drawn - /// over the base, never standalone. + /// A -1 result resolves to the base layer the same as pass 0 does (see [#getIcon], [#getRenderColor]), not the + /// tinted overlay. [ShapeBlockItemRenderer] covers every item-form context Forge dispatches to a custom + /// [net.minecraftforge.client.IItemRenderer] -- GUI slot, hotbar, held, and dropped -- and always sets + /// [#itemRenderPass] explicitly for both of its draws (see its javadoc for the dispatch chain), so -1 is not + /// reached there. It remains a defensive default for any other code that calls [#getIcon]/[#getRenderColor] + /// directly on a full cube outside both world tessellation and [ShapeBlockItemRenderer] -- e.g. a mod that + /// renders an item's icon without going through `RenderItem` at all. The untinted base is a recognizable icon + /// in that case, where the tinted overlay alone (this class's behavior before this fallback existed) rendered + /// as a transparent slot with a few floating tinted flecks, since the overlay icon is a sparse, mostly-transparent + /// layer meant to be drawn over the base, never standalone. private int renderPass() { return itemRenderPass != -1 ? itemRenderPass : ForgeHooksClient.getWorldRenderPass(); } diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java index 5ec5c53b..64514976 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java @@ -11,18 +11,33 @@ import org.lwjgl.opengl.GL11; /// Renders a variant block's item form -- a normal full cube -- with the same base-and-overlay composite as -/// [ShapeBlock]'s world rendering, for every render type Forge dispatches to a custom [IItemRenderer]: a dropped -/// item entity, and the item equipped in a player's hand (third and first person). Vanilla's own inventory and -/// hotbar slot icon never reaches this class at all -- see [ShapeBlock#renderPass]'s javadoc for that path and -/// how [ShapeBlock] degrades gracefully there instead. +/// [ShapeBlock]'s world rendering, for every [ItemRenderType] Forge dispatches to a custom [IItemRenderer]: +/// [ItemRenderType#ENTITY] (a dropped item entity), [ItemRenderType#EQUIPPED] and +/// [ItemRenderType#EQUIPPED_FIRST_PERSON] (held in a player's hand, third and first person), and +/// [ItemRenderType#INVENTORY] -- a GUI inventory slot or hotbar icon. [#handleRenderType] answers every type the +/// same way and [#renderItem] does not branch on `type`, so registering one instance per block (see below) covers +/// all four uniformly. +/// +/// Vanilla's inventory/hotbar icon path (`RenderItem#renderItemIntoGUI`) draws exactly one icon per face with no +/// second pass, but that method is not what a GUI slot or hotbar actually calls: they call +/// `RenderItem#renderItemAndEffectIntoGUI`, which tries +/// `net.minecraftforge.client.ForgeHooksClient#renderInventoryItem` first -- and that consults +/// `net.minecraftforge.client.MinecraftForgeClient#getItemRenderer(item, ItemRenderType.INVENTORY)`, i.e. this +/// class, before ever reaching the single-icon fallback. The same is true for `ItemRenderType.ENTITY` +/// (`ForgeHooksClient#renderEntityItem`) and `ItemRenderType.EQUIPPED` +/// (`net.minecraft.client.renderer.ItemRenderer#renderItem`, which checks `MinecraftForgeClient#getItemRenderer` +/// before falling back to a block's render type at all). All of this is verified against the RetroFuturaGradle +/// dev environment's Forge-patched sources (`build/rfg/mcp_patched_minecraft-sources.jar`) -- not the unpatched +/// vanilla decompile, which lacks every one of those hooks and previously led to the wrong conclusion that +/// inventory and hotbar slots could not reach a custom [IItemRenderer]. Angelica, which this pack runs, does not +/// mixin into `RenderItem`, `ItemRenderer`, or `GuiContainer`, so none of this dispatch chain is altered by it. /// /// Vanilla's block-as-item renderer ([RenderBlocks#renderBlockAsItem]) draws exactly one icon per face, driven by /// a single call to [net.minecraft.block.Block#getRenderColor], so it cannot reproduce [ShapeBlock#getIcon]'s -/// pass-conditioned base/overlay split by itself; unlike world chunk tessellation, it never sets -/// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], so [ShapeBlock#getIcon] always resolves the -/// tinted material icon and the untinted base never shows. This renderer calls `renderBlockAsItem` twice instead, -/// toggling [ShapeBlock#setItemRenderPass] between calls so [ShapeBlock#getIcon] and [ShapeBlock#getRenderColor] -/// pick the same base/overlay layer each pass that world tessellation would. +/// pass-conditioned base/overlay split by itself. This renderer calls `renderBlockAsItem` twice instead, toggling +/// [ShapeBlock#setItemRenderPass] between calls so [ShapeBlock#getIcon] and [ShapeBlock#getRenderColor] pick the +/// base layer for the first call and the tinted overlay for the second, the same split world chunk tessellation +/// gets from its own two render passes (see [ShapeBlock#renderPass]). /// /// The two passes draw the same full cube geometry at the same depth, which -- unlike world chunk tessellation's /// single tessellated pass -- z-fights under the default depth test: two independent draw calls' fragments land diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java new file mode 100644 index 00000000..48d9e875 --- /dev/null +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java @@ -0,0 +1,47 @@ +package com.ruling_0.materiallib.api; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import net.minecraftforge.client.IItemRenderer.ItemRenderType; +import net.minecraftforge.client.IItemRenderer.ItemRendererHelper; + +import org.junit.jupiter.api.Test; + +/// Pins [ShapeBlockItemRenderer]'s render-type gating, the mechanism that composites a variant ore block's item +/// form in a GUI slot and the hotbar, not just when held or dropped. [ShapeBlock] is registered per block through +/// [com.ruling_0.materiallib.ClientProxy] against a single [net.minecraftforge.client.IItemRenderer] instance -- +/// there is no separate opt-in for [ItemRenderType#INVENTORY] -- so +/// [net.minecraftforge.client.MinecraftForgeClient#getItemRenderer] only returns this renderer for a slot or +/// hotbar icon if [ShapeBlockItemRenderer#handleRenderType] answers `true` there the same as it does for +/// [ItemRenderType#EQUIPPED] and [ItemRenderType#ENTITY]; see [ShapeBlockItemRenderer]'s javadoc for the full +/// dispatch chain this was verified against. A future change that narrowed `handleRenderType` back down to only +/// the held/dropped cases -- the mistake an earlier round made from reading the wrong decompiled source -- would +/// silently drop GUI/hotbar compositing again without failing to compile or breaking anything else observable +/// outside a running client, which is exactly what this test guards against headlessly. +class ShapeBlockItemRendererTest { + + private final ShapeBlockItemRenderer renderer = new ShapeBlockItemRenderer( + new ShapeBlock( + "testmod", + "ore_stone", + "%s Ore", + new String[] { "ore" }, + "ore", + "stone", + "minecraft:stone", + BlockBehavior.NONE, + null)); + + @Test + void handlesEveryItemRenderTypeIncludingInventory() { + for (ItemRenderType type : ItemRenderType.values()) { + assertTrue(renderer.handleRenderType(null, type), "expected handleRenderType(" + type + ") to be true"); + } + } + + @Test + void usesTheBlockRenderHelperForInventoryAndEquippedSlots() { + assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.INVENTORY, null, ItemRendererHelper.INVENTORY_BLOCK)); + assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.EQUIPPED, null, ItemRendererHelper.EQUIPPED_BLOCK)); + } +} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index 421ff922..2da1ac45 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -18,8 +18,8 @@ /// only the pass selection, not the tint value, is under test. The -1 (unset) default and world-tessellation /// behavior both need a live client -- /// the user must verify in-client that a variant ore block shows its base texture under the tinted material icon -/// placed in world, in inventory/hotbar (base only -- see [ShapeBlock#renderPass]'s javadoc), and held/dropped -/// (full composite, via [ShapeBlockItemRenderer]). +/// placed in world and, via [ShapeBlockItemRenderer]'s two-pass composite, in every item-form context: GUI slot, +/// hotbar, held, and dropped. class ShapeBlockTest { private final ShapeBlock withBaseTexture = new ShapeBlock( From 00e23f1fe6e91366f4543beab74c36cf9aa4f17d Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:29:32 -0500 Subject: [PATCH 05/18] Fix testOre variant base texture paths that resolved to nonexistent files --- .../materiallib/api/BlockShapeBuilder.java | 8 +++-- .../materiallib/examples/ExampleContent.java | 16 +++++++-- .../examples/ExampleContentTest.java | 34 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java diff --git a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java index 888a6902..09513c33 100644 --- a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java +++ b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java @@ -61,9 +61,11 @@ public BlockShapeBuilder variants(String... variants) { /// Declares the untinted background texture drawn under `variant`'s tinted material icon (e.g. the stone /// background of an ore), as a `domain:path` icon identifier (`"minecraft:stone"`) independent of any - /// material's texture set; the `blocks/` folder is implicit. `texture` is registered as a second render pass; - /// see [ShapeBlock#canRenderInPass]. Optional -- a variant with no base texture renders as a single tinted - /// layer. `variant` must be one of the names passed to [#variants]. + /// material's texture set -- the same convention [TextureSet#iconPath] uses, resolved the same way; do not + /// include the `blocks/` folder, it is implicit (see + /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]). `texture` is + /// registered as a second render pass; see [ShapeBlock#canRenderInPass]. Optional -- a variant with no base + /// texture renders as a single tinted layer. `variant` must be one of the names passed to [#variants]. public BlockShapeBuilder variantBase(String variant, String texture) { Objects.requireNonNull(variant, "variant must not be null"); if (texture == null || texture.isEmpty()) { diff --git a/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java b/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java index 29630038..09fe6ae5 100644 --- a/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java +++ b/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java @@ -28,6 +28,18 @@ /// TestGold show per-pair display names. public final class ExampleContent { + /// The base texture testOre's `stone` variant draws under its tinted material icon; see [#register]. Package + /// visible so [ExampleContentTest][com.ruling_0.materiallib.examples.ExampleContentTest] pins it against + /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]'s convention -- a + /// path with no `blocks/` segment, since that prefix is implicit in the base path the resource-location lookup + /// already applies, naming a file that actually ships in this Minecraft version's vanilla jar. A doubled + /// `blocks/` segment, or a texture 1.7.10 never shipped (granite/diorite/andesite stone variants are a 1.8+ + /// feature), resolves to a nonexistent file and silently falls back to the transparent placeholder icon. + static final String TEST_ORE_STONE_BASE_TEXTURE = "minecraft:stone"; + + /// As [#TEST_ORE_STONE_BASE_TEXTURE], for testOre's `cobblestone` variant. + static final String TEST_ORE_COBBLESTONE_BASE_TEXTURE = "minecraft:cobblestone"; + @SubscribeEvent public void onMaterialRegistration(MaterialRegistrationEvent event) { register(); @@ -59,8 +71,8 @@ private static void register() { .displayName("%s Ore") .oreDict("ore") .variants("stone", "cobblestone") - .variantBase("stone", "minecraft:stone") - .variantBase("cobblestone", "minecraft:cobblestone") + .variantBase("stone", TEST_ORE_STONE_BASE_TEXTURE) + .variantBase("cobblestone", TEST_ORE_COBBLESTONE_BASE_TEXTURE) .iconPath((shape, material) -> "TestGold".equals(material.getName()) ? "minecraft:gold_block" : null) .build(); diff --git a/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java new file mode 100644 index 00000000..0cf96703 --- /dev/null +++ b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java @@ -0,0 +1,34 @@ +package com.ruling_0.materiallib.examples; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.minecraft.util.ResourceLocation; + +import com.gtnewhorizon.gtnhlib.util.ResourceUtil; + +import org.junit.jupiter.api.Test; + +/// Pins testOre's declared variant base textures against +/// [ResourceUtil#getCompleteBlockTextureResourceLocation]'s convention, independent of any live Minecraft resource +/// manager (see `ShapeBlockTest`'s javadoc for why the existence check itself still needs a live client). A +/// `blocks/` segment folded into the identifier -- e.g. `"minecraft:blocks/stone"` instead of `"minecraft:stone"` +/// -- resolves to a path no vanilla jar has (`textures/blocks/blocks/stone.png`) and silently falls back to the +/// transparent placeholder icon; this test would have failed against that regression. +class ExampleContentTest { + + @Test + void testOreStoneBaseTextureResolvesToTheVanillaStoneTexture() { + ResourceLocation location = ResourceUtil + .getCompleteBlockTextureResourceLocation(ExampleContent.TEST_ORE_STONE_BASE_TEXTURE); + assertEquals("minecraft", location.getResourceDomain()); + assertEquals("textures/blocks/stone.png", location.getResourcePath()); + } + + @Test + void testOreCobblestoneBaseTextureResolvesToTheVanillaCobblestoneTexture() { + ResourceLocation location = ResourceUtil + .getCompleteBlockTextureResourceLocation(ExampleContent.TEST_ORE_COBBLESTONE_BASE_TEXTURE); + assertEquals("minecraft", location.getResourceDomain()); + assertEquals("textures/blocks/cobblestone.png", location.getResourcePath()); + } +} From f9ce5dd9d95c680e20d5089f2c182c40c03c25a4 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Fri, 10 Jul 2026 00:29:54 -0500 Subject: [PATCH 06/18] Fix item-form overlay z-fighting by scaling the overlay pass around the origin --- .../materiallib/api/ShapeBlockItemRenderer.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java index 64514976..998a4d70 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java @@ -42,10 +42,15 @@ /// The two passes draw the same full cube geometry at the same depth, which -- unlike world chunk tessellation's /// single tessellated pass -- z-fights under the default depth test: two independent draw calls' fragments land /// at the same depth up to floating-point rounding, so the depth test can reject either one unpredictably. The -/// overlay pass is drawn with [org.lwjgl.opengl.GL11#GL_LEQUAL] depth testing (so an equal-depth fragment always -/// wins over the base pass already in the depth buffer) and scaled up by [#OVERLAY_SCALE] around the cube's -/// center (so it wins even under stricter depth tests some other mod's GL state might have left active), both -/// restored once the composite is done. +/// overlay pass is drawn with [org.lwjgl.opengl.GL11#GL_LEQUAL] depth testing and scaled up by [#OVERLAY_SCALE] +/// so every overlay face sits strictly outside the base cube's, both restored once the composite is done. The +/// scale is applied around the origin because that is where `renderBlockAsItem` centers the cube it draws: for a +/// standard block it rotates, then translates by (-0.5, -0.5, -0.5), then emits the unit-cube faces, so the cube +/// spans -0.5..+0.5 in this renderer's frame -- it does NOT span 0..1. Scaling around (0.5, 0.5, 0.5) would map +/// the face planes at +0.5 exactly onto themselves, and those still-coincident faces speckle per pixel wherever +/// the two passes' slightly different matrices round their equal depths in opposite directions -- most visibly +/// under the GUI slot/hotbar transform (`ForgeHooksClient#renderInventoryItem`), which magnifies the cube 10x +/// through a Z-mirrored ortho projection. /// /// [com.ruling_0.materiallib.ClientProxy] registers one instance per variant block whose shape has a base texture /// ([ShapeBlock#hasBaseTexture]); a plain block shape has nothing to composite and keeps the vanilla single-pass @@ -90,9 +95,7 @@ public void renderItem(ItemRenderType type, ItemStack item, Object... data) { OpenGlHelper.glBlendFunc(770, 771, 1, 0); GL11.glDepthFunc(GL11.GL_LEQUAL); GL11.glPushMatrix(); - GL11.glTranslatef(0.5F, 0.5F, 0.5F); GL11.glScalef(OVERLAY_SCALE, OVERLAY_SCALE, OVERLAY_SCALE); - GL11.glTranslatef(-0.5F, -0.5F, -0.5F); renderBlocks.renderBlockAsItem(shape, meta, 1.0F); GL11.glPopMatrix(); } From 332c7217577c72f4f6cee05d64c0130730464494 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sat, 11 Jul 2026 16:28:02 -0500 Subject: [PATCH 07/18] Defeat Angelica's item-render display-list cache in ShapeBlockItemRenderer --- .../materiallib/api/ShapeBlockItemRenderer.java | 16 ++++++++++++++-- .../textures/blocks/materials/test/testOre.png | Bin 333 -> 504 bytes 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java index 998a4d70..71927857 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java @@ -55,10 +55,22 @@ /// [com.ruling_0.materiallib.ClientProxy] registers one instance per variant block whose shape has a base texture /// ([ShapeBlock#hasBaseTexture]); a plain block shape has nothing to composite and keeps the vanilla single-pass /// item renderer. +/// +/// Angelica (verified at its pack-shipped tag `2.1.49`) mixes into [RenderBlocks#renderBlockAsItem] itself +/// (`angelica.itemrenderer.MixinRenderBlocks`, on by default via `optimizeInWorldItemRendering`) and caches its +/// draw calls into a GL display list keyed only by `(Block, meta)`, replayed on any later call with +/// `brightness == 1.0F`, `useInventoryTint == true`, no AO, and no override texture -- exactly the state both +/// passes below use. Since the two passes share the same `(block, meta)` key and that key knows nothing about +/// [ShapeBlock#setItemRenderPass], the second call hits the first pass's cached list and replays the base layer +/// again instead of re-deriving the tinted overlay, so GUI/hotbar icons render as the untinted base only. Passing +/// [#BRIGHTNESS] instead of `1.0F` is imperceptible (an ULP below 1.0, i.e. a no-op in an environment without this +/// mixin) but permanently fails Angelica's cache-eligibility check, forcing both passes through its real, +/// per-call `RenderBlocks` draw every time. @SideOnly(Side.CLIENT) public final class ShapeBlockItemRenderer implements IItemRenderer { private static final float OVERLAY_SCALE = 1.002F; + private static final float BRIGHTNESS = Math.nextDown(1.0F); private final ShapeBlock shape; @@ -87,7 +99,7 @@ public void renderItem(ItemRenderType type, ItemStack item, Object... data) { shape.setItemRenderPass(0); GL11.glDisable(GL11.GL_BLEND); GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); - renderBlocks.renderBlockAsItem(shape, meta, 1.0F); + renderBlocks.renderBlockAsItem(shape, meta, BRIGHTNESS); shape.setItemRenderPass(1); GL11.glEnable(GL11.GL_BLEND); @@ -96,7 +108,7 @@ public void renderItem(ItemRenderType type, ItemStack item, Object... data) { GL11.glDepthFunc(GL11.GL_LEQUAL); GL11.glPushMatrix(); GL11.glScalef(OVERLAY_SCALE, OVERLAY_SCALE, OVERLAY_SCALE); - renderBlocks.renderBlockAsItem(shape, meta, 1.0F); + renderBlocks.renderBlockAsItem(shape, meta, BRIGHTNESS); GL11.glPopMatrix(); } finally { diff --git a/src/main/resources/assets/materiallib/textures/blocks/materials/test/testOre.png b/src/main/resources/assets/materiallib/textures/blocks/materials/test/testOre.png index 7816799ef142d7c6b44adab2106a417a257f6af5..9ada676c46139edf65e72968b0345ee1aa9f93e5 100644 GIT binary patch literal 504 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%G}50G|-oXV0G9xpU{ig9kTn-n?<+#{2j0-@SYH;lqcIA3xr^ckk}qyPrON zdh_NDQ2B$6zkUKKzLFrn;Qz>g!S%qM6F?cx0*}aIpn}67%y>M1MG8<*vcxr_Bsf2< zq&%@Gl_4N8Gp|I?&oxBBRL@Y)&`6lky#%OeM{0y;ny0500|$`9${@wa%D@O@c>%FB zlnwHp1|u_AoC(M_WMpCx1kzDJoY~F-7S92)K_CH$LHc1dx~&Whz#y8y&cFgxV_;-# zz_uv3Tt{!cMt7q11^v!T+Q(eyG@c8}7O(JDBa>m@9?ON6#k1}|=`njxgN@xNA+i;U; delta 317 zcmV-D0mA(`&ryK8uIpqN z#tz;~Q4~?rG)&iZGzi3lePJn8@O3$+Ft$YD&Anq2HW!Wp=w(W_JMGS)A zf4gv$T@(d#v>-Zww`>6l)^(+A+Zdb$eczvWRbbmTNnsfN*@9(RPYdscsIF^9*g@q5 zu&{Rl3LsYu&o Date: Sat, 11 Jul 2026 16:38:43 -0500 Subject: [PATCH 08/18] Pin ShapeBlockItemRenderer's off-1.0 brightness against future regression --- .../api/ShapeBlockItemRendererTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java index 48d9e875..37803e8e 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java @@ -1,7 +1,10 @@ package com.ruling_0.materiallib.api; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.lang.reflect.Field; + import net.minecraftforge.client.IItemRenderer.ItemRenderType; import net.minecraftforge.client.IItemRenderer.ItemRendererHelper; @@ -44,4 +47,15 @@ void usesTheBlockRenderHelperForInventoryAndEquippedSlots() { assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.INVENTORY, null, ItemRendererHelper.INVENTORY_BLOCK)); assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.EQUIPPED, null, ItemRendererHelper.EQUIPPED_BLOCK)); } + + /// Pins that both composite passes draw at a brightness distinguishable from exactly `1.0F` (see the class + /// javadoc's Angelica display-list-cache explanation): reverting to a literal `1.0F` would silently reopen the + /// pack-only blank-inventory bug without failing to compile or affecting the plain dev client, where every + /// brightness value renders identically. + @Test + void brightnessIsNotExactlyOne() throws ReflectiveOperationException { + Field field = ShapeBlockItemRenderer.class.getDeclaredField("BRIGHTNESS"); + field.setAccessible(true); + assertNotEquals(1.0F, (float) field.get(null)); + } } From ebac047cb5709538317e3aa777d4de1fd5ac361a Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:26:36 -0500 Subject: [PATCH 09/18] Replace composite ShapeBlock item rendering with an ISBRH --- .../com/ruling_0/materiallib/ClientProxy.java | 10 +- .../materiallib/api/BlockShapeBuilder.java | 5 +- .../ruling_0/materiallib/api/ShapeBlock.java | 92 +++++-------- .../api/ShapeBlockItemRenderer.java | 122 ----------------- .../api/ShapeBlockRenderingHandler.java | 126 ++++++++++++++++++ .../api/ShapeBlockItemRendererTest.java | 61 --------- .../materiallib/api/ShapeBlockTest.java | 54 ++++---- 7 files changed, 194 insertions(+), 276 deletions(-) delete mode 100644 src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java create mode 100644 src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java delete mode 100644 src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java diff --git a/src/main/java/com/ruling_0/materiallib/ClientProxy.java b/src/main/java/com/ruling_0/materiallib/ClientProxy.java index c454ecee..db4f747d 100644 --- a/src/main/java/com/ruling_0/materiallib/ClientProxy.java +++ b/src/main/java/com/ruling_0/materiallib/ClientProxy.java @@ -1,17 +1,16 @@ package com.ruling_0.materiallib; -import net.minecraft.item.Item; - import net.minecraftforge.client.MinecraftForgeClient; import net.minecraftforge.common.MinecraftForge; import com.ruling_0.materiallib.api.ShapeBlock; -import com.ruling_0.materiallib.api.ShapeBlockItemRenderer; +import com.ruling_0.materiallib.api.ShapeBlockRenderingHandler; import com.ruling_0.materiallib.api.ShapeFluidIcons; import com.ruling_0.materiallib.api.ShapeItem; import com.ruling_0.materiallib.api.ShapeItemRenderer; import com.ruling_0.materiallib.api.ShapeRegistry; +import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.event.FMLPreInitializationEvent; public class ClientProxy extends CommonProxy { @@ -23,10 +22,11 @@ public void preInit(FMLPreInitializationEvent event) { for (ShapeItem item : ShapeRegistry.instance().getItemShapes()) { MinecraftForgeClient.registerItemRenderer(item, renderer); } + ShapeBlockRenderingHandler blockRenderer = new ShapeBlockRenderingHandler(); + RenderingRegistry.registerBlockHandler(blockRenderer); for (ShapeBlock block : ShapeRegistry.instance().getBlockShapes()) { if (block.hasBaseTexture()) { - MinecraftForgeClient.registerItemRenderer(Item.getItemFromBlock(block), - new ShapeBlockItemRenderer(block)); + block.setRenderType(blockRenderer.getRenderId()); } } MinecraftForge.EVENT_BUS.register(new ShapeFluidIcons()); diff --git a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java index 09513c33..4777acab 100644 --- a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java +++ b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java @@ -64,8 +64,9 @@ public BlockShapeBuilder variants(String... variants) { /// material's texture set -- the same convention [TextureSet#iconPath] uses, resolved the same way; do not /// include the `blocks/` folder, it is implicit (see /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]). `texture` is - /// registered as a second render pass; see [ShapeBlock#canRenderInPass]. Optional -- a variant with no base - /// texture renders as a single tinted layer. `variant` must be one of the names passed to [#variants]. + /// composited under the tinted material icon by [ShapeBlockRenderingHandler]; see [ShapeBlock#hasBaseTexture]. + /// Optional -- a variant with no base texture renders as a single tinted layer. `variant` must be one of the + /// names passed to [#variants]. public BlockShapeBuilder variantBase(String variant, String texture) { Objects.requireNonNull(variant, "variant must not be null"); if (texture == null || texture.isEmpty()) { diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 86b97e91..95497622 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -15,8 +15,6 @@ import net.minecraft.world.IBlockAccess; import net.minecraft.world.World; -import net.minecraftforge.client.ForgeHooksClient; - import com.gtnewhorizon.gtnhlib.util.ResourceUtil; import com.ruling_0.materiallib.MaterialLib; @@ -37,13 +35,13 @@ /// instances, and the block's item shows the same display name and advanced-tooltip attribution as an item shape. /// /// A variant block built by [ShapeBlockVariants] additionally falls back from its own icon (`_`) -/// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) in the solid render -/// pass, under the tinted material icon drawn in the alpha pass; see [#registerBlockIcons] and [#canRenderInPass]. -/// The same base-and-overlay composite is reproduced for the item form -- GUI slot, hotbar, held, and dropped -- -/// by [ShapeBlockItemRenderer]; see [#renderPass] for how the two mechanisms share [#getIcon] and [#getRenderColor]. -/// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest -/// tool class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla -/// default it replaces. +/// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) under the tinted +/// material icon; see [#registerBlockIcons]. [ShapeBlockRenderingHandler] composites both layers -- in world and in +/// every item form (GUI slot, hotbar, held, and dropped) -- in a single draw per block, driven by [#getIcon], +/// [#getRenderColor], and [#colorMultiplier] through [#layerOverride]; see [#hasBaseTexture]. Drops, hardness, +/// resistance, and harvest level may be overridden per material and variant, and the harvest tool class per +/// shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla default it +/// replaces. public class ShapeBlock extends Block implements BackedShape { private final String modid; @@ -60,7 +58,8 @@ public class ShapeBlock extends Block implements BackedShape { private final ShapeIcons icons = new ShapeIcons(false); private IIcon baseIcon; private boolean warnedMissingBaseTexture; - private int itemRenderPass = -1; + private int layerOverride = -1; + private int renderType = 0; /// Creates a block shape backed by a [net.minecraft.block.material.Material#iron] block. `oreDicts` are the /// oredict prefixes, at least one; `displayNameFormat` is applied to the material name to build the display @@ -199,18 +198,30 @@ private IIcon registerBaseIcon(IIconRegister register) { /// Whether this variant draws a base texture layer under the tinted material icon; see [#registerBlockIcons]. /// [ClientProxy][com.ruling_0.materiallib.ClientProxy] uses this to decide which block shapes need - /// [ShapeBlockItemRenderer] -- a plain block shape has nothing to composite and keeps the vanilla single-pass - /// item renderer. + /// [#setRenderType] pointed at [ShapeBlockRenderingHandler] -- a plain block shape has nothing to composite and + /// keeps the vanilla full-cube render type (0). public boolean hasBaseTexture() { return baseTexture != null; } - /// Sets the pass [#renderPass] falls back to when [ForgeHooksClient#getWorldRenderPass] is -1 (i.e. outside - /// world chunk tessellation), or -1 to clear it. [ShapeBlockItemRenderer] toggles this around each of its two - /// [net.minecraft.client.renderer.RenderBlocks#renderBlockAsItem] calls so the item form's base and overlay - /// layers resolve the same icon and color [#getIcon]/[#getRenderColor] give the corresponding world render - /// pass; see [#renderPass] for what -1 resolves to everywhere else. - void setItemRenderPass(int pass) { itemRenderPass = pass; } + /// This block's [#getRenderType]; [ClientProxy][com.ruling_0.materiallib.ClientProxy] sets this once, at + /// preInit, to [ShapeBlockRenderingHandler]'s render ID for every block [#hasBaseTexture]. Never called on the + /// dedicated server, where [ShapeBlockRenderingHandler] itself must never classload. + public void setRenderType(int renderType) { this.renderType = renderType; } + + @Override + public int getRenderType() { return renderType; } + + /// Forces [#getIcon], [#getRenderColor], and [#colorMultiplier] to resolve the base layer (0) or the tinted + /// overlay layer (1) regardless of metadata, or -1 to resolve the base layer by default. + /// [ShapeBlockRenderingHandler] toggles this around each of the two draws its single [#hasBaseTexture] + /// composite needs -- both the world and the item-form (GUI slot, hotbar, held, and dropped) renderers set it + /// explicitly before every draw, so -1 is only reached by other code that calls [#getIcon] or [#getRenderColor] + /// directly, e.g. a mod inspecting an icon outside `RenderItem` entirely. The untinted base is a recognizable + /// icon in that case, where the tinted overlay alone renders as a transparent slot with a few floating tinted + /// flecks, since the overlay icon is a sparse, mostly-transparent layer meant to be drawn over the base, never + /// standalone. + void setLayerOverride(int layer) { layerOverride = layer; } /// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight /// to them. The default implementation defers to this block's [BlockIconPather]. A subclass may override this @@ -219,48 +230,19 @@ protected String iconPathFor(Material material) { return iconPather != null ? iconPather.iconPath(this, material) : null; } - /// A block with no base texture renders as a single tinted layer. A block with a base texture - /// renders in two passes -- the untinted base in the solid pass 0, and the tinted material icon over it in - /// the alpha-blended pass 1 (pass 1 draws after pass 0, and the material texture's transparent pixels let - /// the base show through). [#getIcon] and [#colorMultiplier] tell the two passes apart through - /// [ForgeHooksClient#getWorldRenderPass], which [ForgeHooksClient] only sets to 0 or 1 around world chunk - /// tessellation; it is -1 everywhere else, including the item form, which falls back to [#itemRenderPass] - /// instead (see [#getIcon], [#getRenderColor], [#setItemRenderPass]); see [#canRenderInPass]. + /// Signals to vanilla and Forge's item-rendering dispatch (`RenderItem`, `ItemRenderer`) that this block's item + /// form has translucent content whenever it [#hasBaseTexture] -- those callers enable alpha blending around the + /// composite draw precisely when this returns nonzero, and [ShapeBlockRenderingHandler]'s single world draw + /// participates in exactly the render pass this names ([net.minecraft.block.Block#canRenderInPass] defaults to + /// `pass == getRenderBlockPass()`), so the overlay's transparent pixels are blended, not just alpha-tested. @Override @SideOnly(Side.CLIENT) public int getRenderBlockPass() { return baseTexture != null ? 1 : 0; } - @Override - @SideOnly(Side.CLIENT) - public boolean canRenderInPass(int pass) { - return baseTexture == null ? pass == 0 : pass == 0 || pass == 1; - } - - /// [#itemRenderPass] when [ShapeBlockItemRenderer] has set it (0 or 1, driving the item form's two-pass - /// composite), or the world-tessellation render pass ([ForgeHooksClient#getWorldRenderPass]) otherwise -- 0 - /// or 1 during world chunk tessellation, -1 everywhere else. [ShapeBlockItemRenderer] and world tessellation - /// never run nested inside one another, so which of the two this checks first never changes the result; item - /// pass first also means a call with [#itemRenderPass] set never touches [ForgeHooksClient], which needs a - /// live client and is otherwise unreachable from a headless test (see [ShapeBlockTest]'s javadoc). - /// - /// A -1 result resolves to the base layer the same as pass 0 does (see [#getIcon], [#getRenderColor]), not the - /// tinted overlay. [ShapeBlockItemRenderer] covers every item-form context Forge dispatches to a custom - /// [net.minecraftforge.client.IItemRenderer] -- GUI slot, hotbar, held, and dropped -- and always sets - /// [#itemRenderPass] explicitly for both of its draws (see its javadoc for the dispatch chain), so -1 is not - /// reached there. It remains a defensive default for any other code that calls [#getIcon]/[#getRenderColor] - /// directly on a full cube outside both world tessellation and [ShapeBlockItemRenderer] -- e.g. a mod that - /// renders an item's icon without going through `RenderItem` at all. The untinted base is a recognizable icon - /// in that case, where the tinted overlay alone (this class's behavior before this fallback existed) rendered - /// as a transparent slot with a few floating tinted flecks, since the overlay icon is a sparse, mostly-transparent - /// layer meant to be drawn over the base, never standalone. - private int renderPass() { - return itemRenderPass != -1 ? itemRenderPass : ForgeHooksClient.getWorldRenderPass(); - } - @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { - if (baseTexture != null && renderPass() != 1) { + if (baseTexture != null && layerOverride != 1) { return baseIcon; } return icons.get(meta); @@ -269,7 +251,7 @@ public IIcon getIcon(int side, int meta) { @Override @SideOnly(Side.CLIENT) public int getRenderColor(int meta) { - if (baseTexture != null && renderPass() != 1) { + if (baseTexture != null && layerOverride != 1) { return 0xFFFFFF; } return tintFor(meta); @@ -278,7 +260,7 @@ public int getRenderColor(int meta) { @Override @SideOnly(Side.CLIENT) public int colorMultiplier(IBlockAccess world, int x, int y, int z) { - if (baseTexture != null && ForgeHooksClient.getWorldRenderPass() == 0) { + if (baseTexture != null && layerOverride != 1) { return 0xFFFFFF; } return tintFor(world.getBlockMetadata(x, y, z)); diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java deleted file mode 100644 index 71927857..00000000 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockItemRenderer.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.ruling_0.materiallib.api; - -import net.minecraft.client.renderer.OpenGlHelper; -import net.minecraft.client.renderer.RenderBlocks; -import net.minecraft.item.ItemStack; - -import net.minecraftforge.client.IItemRenderer; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import org.lwjgl.opengl.GL11; - -/// Renders a variant block's item form -- a normal full cube -- with the same base-and-overlay composite as -/// [ShapeBlock]'s world rendering, for every [ItemRenderType] Forge dispatches to a custom [IItemRenderer]: -/// [ItemRenderType#ENTITY] (a dropped item entity), [ItemRenderType#EQUIPPED] and -/// [ItemRenderType#EQUIPPED_FIRST_PERSON] (held in a player's hand, third and first person), and -/// [ItemRenderType#INVENTORY] -- a GUI inventory slot or hotbar icon. [#handleRenderType] answers every type the -/// same way and [#renderItem] does not branch on `type`, so registering one instance per block (see below) covers -/// all four uniformly. -/// -/// Vanilla's inventory/hotbar icon path (`RenderItem#renderItemIntoGUI`) draws exactly one icon per face with no -/// second pass, but that method is not what a GUI slot or hotbar actually calls: they call -/// `RenderItem#renderItemAndEffectIntoGUI`, which tries -/// `net.minecraftforge.client.ForgeHooksClient#renderInventoryItem` first -- and that consults -/// `net.minecraftforge.client.MinecraftForgeClient#getItemRenderer(item, ItemRenderType.INVENTORY)`, i.e. this -/// class, before ever reaching the single-icon fallback. The same is true for `ItemRenderType.ENTITY` -/// (`ForgeHooksClient#renderEntityItem`) and `ItemRenderType.EQUIPPED` -/// (`net.minecraft.client.renderer.ItemRenderer#renderItem`, which checks `MinecraftForgeClient#getItemRenderer` -/// before falling back to a block's render type at all). All of this is verified against the RetroFuturaGradle -/// dev environment's Forge-patched sources (`build/rfg/mcp_patched_minecraft-sources.jar`) -- not the unpatched -/// vanilla decompile, which lacks every one of those hooks and previously led to the wrong conclusion that -/// inventory and hotbar slots could not reach a custom [IItemRenderer]. Angelica, which this pack runs, does not -/// mixin into `RenderItem`, `ItemRenderer`, or `GuiContainer`, so none of this dispatch chain is altered by it. -/// -/// Vanilla's block-as-item renderer ([RenderBlocks#renderBlockAsItem]) draws exactly one icon per face, driven by -/// a single call to [net.minecraft.block.Block#getRenderColor], so it cannot reproduce [ShapeBlock#getIcon]'s -/// pass-conditioned base/overlay split by itself. This renderer calls `renderBlockAsItem` twice instead, toggling -/// [ShapeBlock#setItemRenderPass] between calls so [ShapeBlock#getIcon] and [ShapeBlock#getRenderColor] pick the -/// base layer for the first call and the tinted overlay for the second, the same split world chunk tessellation -/// gets from its own two render passes (see [ShapeBlock#renderPass]). -/// -/// The two passes draw the same full cube geometry at the same depth, which -- unlike world chunk tessellation's -/// single tessellated pass -- z-fights under the default depth test: two independent draw calls' fragments land -/// at the same depth up to floating-point rounding, so the depth test can reject either one unpredictably. The -/// overlay pass is drawn with [org.lwjgl.opengl.GL11#GL_LEQUAL] depth testing and scaled up by [#OVERLAY_SCALE] -/// so every overlay face sits strictly outside the base cube's, both restored once the composite is done. The -/// scale is applied around the origin because that is where `renderBlockAsItem` centers the cube it draws: for a -/// standard block it rotates, then translates by (-0.5, -0.5, -0.5), then emits the unit-cube faces, so the cube -/// spans -0.5..+0.5 in this renderer's frame -- it does NOT span 0..1. Scaling around (0.5, 0.5, 0.5) would map -/// the face planes at +0.5 exactly onto themselves, and those still-coincident faces speckle per pixel wherever -/// the two passes' slightly different matrices round their equal depths in opposite directions -- most visibly -/// under the GUI slot/hotbar transform (`ForgeHooksClient#renderInventoryItem`), which magnifies the cube 10x -/// through a Z-mirrored ortho projection. -/// -/// [com.ruling_0.materiallib.ClientProxy] registers one instance per variant block whose shape has a base texture -/// ([ShapeBlock#hasBaseTexture]); a plain block shape has nothing to composite and keeps the vanilla single-pass -/// item renderer. -/// -/// Angelica (verified at its pack-shipped tag `2.1.49`) mixes into [RenderBlocks#renderBlockAsItem] itself -/// (`angelica.itemrenderer.MixinRenderBlocks`, on by default via `optimizeInWorldItemRendering`) and caches its -/// draw calls into a GL display list keyed only by `(Block, meta)`, replayed on any later call with -/// `brightness == 1.0F`, `useInventoryTint == true`, no AO, and no override texture -- exactly the state both -/// passes below use. Since the two passes share the same `(block, meta)` key and that key knows nothing about -/// [ShapeBlock#setItemRenderPass], the second call hits the first pass's cached list and replays the base layer -/// again instead of re-deriving the tinted overlay, so GUI/hotbar icons render as the untinted base only. Passing -/// [#BRIGHTNESS] instead of `1.0F` is imperceptible (an ULP below 1.0, i.e. a no-op in an environment without this -/// mixin) but permanently fails Angelica's cache-eligibility check, forcing both passes through its real, -/// per-call `RenderBlocks` draw every time. -@SideOnly(Side.CLIENT) -public final class ShapeBlockItemRenderer implements IItemRenderer { - - private static final float OVERLAY_SCALE = 1.002F; - private static final float BRIGHTNESS = Math.nextDown(1.0F); - - private final ShapeBlock shape; - - public ShapeBlockItemRenderer(ShapeBlock shape) { - this.shape = shape; - } - - @Override - public boolean handleRenderType(ItemStack item, ItemRenderType type) { - return true; - } - - @Override - public boolean shouldUseRenderHelper(ItemRenderType type, ItemStack item, ItemRendererHelper helper) { - return true; - } - - @Override - public void renderItem(ItemRenderType type, ItemStack item, Object... data) { - RenderBlocks renderBlocks = data.length > 0 && data[0] instanceof RenderBlocks rb ? rb : new RenderBlocks(); - int meta = item.getItemDamage(); - boolean wasInventoryTint = renderBlocks.useInventoryTint; - renderBlocks.useInventoryTint = true; - - try { - shape.setItemRenderPass(0); - GL11.glDisable(GL11.GL_BLEND); - GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); - renderBlocks.renderBlockAsItem(shape, meta, BRIGHTNESS); - - shape.setItemRenderPass(1); - GL11.glEnable(GL11.GL_BLEND); - GL11.glAlphaFunc(GL11.GL_GREATER, 0.1F); - OpenGlHelper.glBlendFunc(770, 771, 1, 0); - GL11.glDepthFunc(GL11.GL_LEQUAL); - GL11.glPushMatrix(); - GL11.glScalef(OVERLAY_SCALE, OVERLAY_SCALE, OVERLAY_SCALE); - renderBlocks.renderBlockAsItem(shape, meta, BRIGHTNESS); - GL11.glPopMatrix(); - } - finally { - shape.setItemRenderPass(-1); - renderBlocks.useInventoryTint = wasInventoryTint; - GL11.glDisable(GL11.GL_BLEND); - GL11.glAlphaFunc(GL11.GL_GREATER, 0.5F); - GL11.glDepthFunc(GL11.GL_LEQUAL); - } - } -} diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java new file mode 100644 index 00000000..3536a802 --- /dev/null +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java @@ -0,0 +1,126 @@ +package com.ruling_0.materiallib.api; + +import net.minecraft.block.Block; +import net.minecraft.client.renderer.RenderBlocks; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.util.IIcon; +import net.minecraft.world.IBlockAccess; + +import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler; +import cpw.mods.fml.client.registry.RenderingRegistry; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.lwjgl.opengl.GL11; + +/// Renders a [ShapeBlock#hasBaseTexture] composite -- an untinted base texture under a tinted material icon -- as a +/// single draw, in world and in every item form (GUI slot, hotbar, held, and dropped). +/// [com.ruling_0.materiallib.ClientProxy] +/// registers one instance of this handler for every such block, through [#RENDER_ID] and [ShapeBlock#setRenderType]; +/// a block with no base texture keeps the vanilla full-cube render type (0) and never reaches this class. +/// +/// Both [#renderWorldBlock] and [#renderInventoryBlock] draw the base layer and the tinted overlay back-to-back +/// into the same [Tessellator] batch, toggling [ShapeBlock#setLayerOverride] between the two so [ShapeBlock#getIcon], +/// [ShapeBlock#getRenderColor], and [ShapeBlock#colorMultiplier] resolve the base layer for the first draw and the +/// overlay for the second -- the composite that used to take two separate render passes (world chunk tessellation's +/// solid and alpha passes, or two `renderBlockAsItem` calls) now happens in one. Because the two layers share a +/// tessellator batch, their coplanar quads are submitted back-to-back with identical vertex data, so the depth test +/// resolves the tie deterministically in submission order instead of z-fighting the way two independent draw calls +/// would; this is the same technique GT5-Unofficial's `gregtech.common.render.GTRendererBlock` uses for its own +/// texture-array composites. +/// +/// [ShapeBlock#getRenderBlockPass] returns 1 for a block [ShapeBlock#hasBaseTexture], both to keep this handler's +/// single [#renderWorldBlock] call inside the alpha-blended chunk pass (`Block#canRenderInPass` defaults to +/// `pass == getRenderBlockPass()`, so a block renders in exactly one pass without any override here) and because +/// vanilla and Forge's item-rendering dispatch (`RenderItem`, `ItemRenderer`) read that same value to decide whether +/// to enable alpha blending around a block-rendered item icon -- without it the overlay's transparent pixels would +/// only be alpha-tested, not blended. +/// +/// This handler holds no mutable state, so one instance is safe to reuse from any thread a world mesher (e.g. +/// Angelica's Celeritas) calls it from. +@SideOnly(Side.CLIENT) +public final class ShapeBlockRenderingHandler implements ISimpleBlockRenderingHandler { + + private static final int RENDER_ID = RenderingRegistry.getNextAvailableRenderId(); + + @Override + public int getRenderId() { return RENDER_ID; } + + @Override + public boolean shouldRender3DInInventory(int modelId) { + return true; + } + + @Override + public void renderInventoryBlock(Block block, int metadata, int modelId, RenderBlocks renderer) { + if (!(block instanceof ShapeBlock shape)) return; + block.setBlockBoundsForItemRender(); + renderer.setRenderBoundsFromBlock(block); + renderer.useInventoryTint = true; + + GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F); + GL11.glTranslatef(-0.5F, -0.5F, -0.5F); + + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + drawInventoryLayer(renderer, tessellator, shape, metadata, 0); + drawInventoryLayer(renderer, tessellator, shape, metadata, 1); + tessellator.draw(); + + GL11.glTranslatef(0.5F, 0.5F, 0.5F); + } + + private static void drawInventoryLayer(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape, + int metadata, int layer) { + shape.setLayerOverride(layer); + try { + int color = shape.getRenderColor(metadata); + tessellator.setColorOpaque_F( + (color >> 16 & 255) / 255.0F, + (color >> 8 & 255) / 255.0F, + (color & 255) / 255.0F); + drawInventoryFace(renderer, tessellator, shape, 0, metadata, 0.0F, -1.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 1, metadata, 0.0F, 1.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 2, metadata, 0.0F, 0.0F, -1.0F); + drawInventoryFace(renderer, tessellator, shape, 3, metadata, 0.0F, 0.0F, 1.0F); + drawInventoryFace(renderer, tessellator, shape, 4, metadata, -1.0F, 0.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 5, metadata, 1.0F, 0.0F, 0.0F); + } + finally { + shape.setLayerOverride(-1); + } + } + + private static void drawInventoryFace(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape, int side, + int metadata, float nx, float ny, float nz) { + IIcon icon = shape.getIcon(side, metadata); + tessellator.setNormal(nx, ny, nz); + switch (side) { + case 0 -> renderer.renderFaceYNeg(shape, 0.0D, 0.0D, 0.0D, icon); + case 1 -> renderer.renderFaceYPos(shape, 0.0D, 0.0D, 0.0D, icon); + case 2 -> renderer.renderFaceZNeg(shape, 0.0D, 0.0D, 0.0D, icon); + case 3 -> renderer.renderFaceZPos(shape, 0.0D, 0.0D, 0.0D, icon); + case 4 -> renderer.renderFaceXNeg(shape, 0.0D, 0.0D, 0.0D, icon); + case 5 -> renderer.renderFaceXPos(shape, 0.0D, 0.0D, 0.0D, icon); + default -> throw new IllegalArgumentException("side must be 0..5, got " + side); + } + } + + @Override + public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, + RenderBlocks renderer) { + if (!(block instanceof ShapeBlock shape)) return false; + boolean renderedBase = drawWorldLayer(renderer, shape, x, y, z, 0); + boolean renderedOverlay = drawWorldLayer(renderer, shape, x, y, z, 1); + return renderedBase || renderedOverlay; + } + + private static boolean drawWorldLayer(RenderBlocks renderer, ShapeBlock shape, int x, int y, int z, int layer) { + shape.setLayerOverride(layer); + try { + return renderer.renderStandardBlock(shape, x, y, z); + } + finally { + shape.setLayerOverride(-1); + } + } +} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java deleted file mode 100644 index 37803e8e..00000000 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockItemRendererTest.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.ruling_0.materiallib.api; - -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.lang.reflect.Field; - -import net.minecraftforge.client.IItemRenderer.ItemRenderType; -import net.minecraftforge.client.IItemRenderer.ItemRendererHelper; - -import org.junit.jupiter.api.Test; - -/// Pins [ShapeBlockItemRenderer]'s render-type gating, the mechanism that composites a variant ore block's item -/// form in a GUI slot and the hotbar, not just when held or dropped. [ShapeBlock] is registered per block through -/// [com.ruling_0.materiallib.ClientProxy] against a single [net.minecraftforge.client.IItemRenderer] instance -- -/// there is no separate opt-in for [ItemRenderType#INVENTORY] -- so -/// [net.minecraftforge.client.MinecraftForgeClient#getItemRenderer] only returns this renderer for a slot or -/// hotbar icon if [ShapeBlockItemRenderer#handleRenderType] answers `true` there the same as it does for -/// [ItemRenderType#EQUIPPED] and [ItemRenderType#ENTITY]; see [ShapeBlockItemRenderer]'s javadoc for the full -/// dispatch chain this was verified against. A future change that narrowed `handleRenderType` back down to only -/// the held/dropped cases -- the mistake an earlier round made from reading the wrong decompiled source -- would -/// silently drop GUI/hotbar compositing again without failing to compile or breaking anything else observable -/// outside a running client, which is exactly what this test guards against headlessly. -class ShapeBlockItemRendererTest { - - private final ShapeBlockItemRenderer renderer = new ShapeBlockItemRenderer( - new ShapeBlock( - "testmod", - "ore_stone", - "%s Ore", - new String[] { "ore" }, - "ore", - "stone", - "minecraft:stone", - BlockBehavior.NONE, - null)); - - @Test - void handlesEveryItemRenderTypeIncludingInventory() { - for (ItemRenderType type : ItemRenderType.values()) { - assertTrue(renderer.handleRenderType(null, type), "expected handleRenderType(" + type + ") to be true"); - } - } - - @Test - void usesTheBlockRenderHelperForInventoryAndEquippedSlots() { - assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.INVENTORY, null, ItemRendererHelper.INVENTORY_BLOCK)); - assertTrue(renderer.shouldUseRenderHelper(ItemRenderType.EQUIPPED, null, ItemRendererHelper.EQUIPPED_BLOCK)); - } - - /// Pins that both composite passes draw at a brightness distinguishable from exactly `1.0F` (see the class - /// javadoc's Angelica display-list-cache explanation): reverting to a literal `1.0F` would silently reopen the - /// pack-only blank-inventory bug without failing to compile or affecting the plain dev client, where every - /// brightness value renders identically. - @Test - void brightnessIsNotExactlyOne() throws ReflectiveOperationException { - Field field = ShapeBlockItemRenderer.class.getDeclaredField("BRIGHTNESS"); - field.setAccessible(true); - assertNotEquals(1.0F, (float) field.get(null)); - } -} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index 2da1ac45..1893e868 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -1,25 +1,19 @@ package com.ruling_0.materiallib.api; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -/// Headless coverage for [ShapeBlock]'s base-texture bookkeeping. [ShapeBlock#renderPass] checks -/// [ShapeBlock#itemRenderPass] before falling back to -/// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass], which needs a live client and is unreachable -/// from this suite (calling it throws `NoClassDefFoundError: org/lwjgl/LWJGLException`, since LWJGL is not on the -/// test runtime classpath); every case here sets [ShapeBlock#itemRenderPass] explicitly so that fallback is never -/// reached. [ShapeBlock#getRenderColor]'s tinted branch resolves a material through the process-wide -/// [MaterialRegistry#instance], which is unresolved (and throws on lookup) until MaterialLib's preInit runs, so -/// no test here can reach it at all; [ShapeBlock#getIcon] never consults a material and stands in for it where -/// only the pass selection, not the tint value, is under test. The -1 (unset) default and world-tessellation -/// behavior both need a live client -- -/// the user must verify in-client that a variant ore block shows its base texture under the tinted material icon -/// placed in world and, via [ShapeBlockItemRenderer]'s two-pass composite, in every item-form context: GUI slot, -/// hotbar, held, and dropped. +/// Headless coverage for [ShapeBlock]'s base-texture and render-type bookkeeping. [ShapeBlock#getRenderColor]'s +/// tinted branch resolves a material through the process-wide [MaterialRegistry#instance], which is unresolved +/// (and throws on lookup) until MaterialLib's preInit runs, so no test here reaches it; only the untinted (layer 0) +/// branch, which returns before consulting any material, is exercised. +/// +/// World compositing, item-form compositing, and [ShapeBlockRenderingHandler]'s registration all need a live +/// client -- the user must verify in-client that a variant ore block shows its base texture under the tinted +/// material icon, in world and in every item-form context: GUI slot, hotbar, held, and dropped. class ShapeBlockTest { private final ShapeBlock withBaseTexture = new ShapeBlock( @@ -41,36 +35,34 @@ void hasBaseTextureReflectsWhetherAVariantBaseWasDeclared() { assertFalse(withoutBaseTexture.hasBaseTexture()); } - /// Pass 0 of the item-form composite ([ShapeBlockItemRenderer]) must draw the base texture untinted, the same - /// as pass 0 of the world composite ([ShapeBlock#colorMultiplier]) does. This branch returns before consulting - /// any material, so it holds regardless of the given metadata, including one bound to no live material. + /// [ShapeBlockRenderingHandler] drives the composite entirely through [ShapeBlock#setLayerOverride]; layer 0 + /// (the base) must render untinted whether or not a base texture exists, the same as + /// [ShapeBlock#colorMultiplier] does for world tessellation's solid layer. @Test - void itemRenderPassZeroIsUntintedWhenABaseTextureExists() { - withBaseTexture.setItemRenderPass(0); + void layerZeroIsUntintedWhenABaseTextureExists() { + withBaseTexture.setLayerOverride(0); try { assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(0)); assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(999)); } finally { - withBaseTexture.setItemRenderPass(-1); + withBaseTexture.setLayerOverride(-1); } } - /// Pass 1 is the overlay pass; [ShapeBlock#renderPass] must resolve it from [ShapeBlock#itemRenderPass] alone, - /// the same as pass 0, never falling through to - /// [net.minecraftforge.client.ForgeHooksClient#getWorldRenderPass] (which would throw here; see this class's - /// javadoc) -- a future change that reordered those two checks would still pass - /// [#itemRenderPassZeroIsUntintedWhenABaseTextureExists] (pass 0 short-circuits either order) but fail here. - /// [ShapeBlock#getIcon] never consults a material, unlike [ShapeBlock#getRenderColor]'s tinted branch, so it - /// is the one exercised here (see this class's javadoc on why the tinted branch cannot be). + /// Pins the render-type wiring [com.ruling_0.materiallib.ClientProxy] relies on: a block defaults to the + /// vanilla full-cube render type (0) and reports whatever [ShapeBlockRenderingHandler]'s render ID + /// [ShapeBlock#setRenderType] was last given, so a future change that stopped wiring composite blocks to their + /// handler would leave them silently rendering as plain cubes instead of failing to compile. @Test - void itemRenderPassOneNeverTouchesTheWorldRenderPass() { - withBaseTexture.setItemRenderPass(1); + void getRenderTypeReflectsWhateverWasSet() { + assertEquals(0, withBaseTexture.getRenderType()); + withBaseTexture.setRenderType(64); try { - assertDoesNotThrow(() -> withBaseTexture.getIcon(0, 0)); + assertEquals(64, withBaseTexture.getRenderType()); } finally { - withBaseTexture.setItemRenderPass(-1); + withBaseTexture.setRenderType(0); } } } From 7664d8f6b358e1ef0ce3d8fd76f77ca998d97449 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:45:25 -0500 Subject: [PATCH 10/18] Keep composite ShapeBlocks in the solid render pass like legacy ores --- .../com/ruling_0/materiallib/api/ShapeBlock.java | 9 --------- .../api/ShapeBlockRenderingHandler.java | 15 +++++++++------ 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 95497622..327e1c3c 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -230,15 +230,6 @@ protected String iconPathFor(Material material) { return iconPather != null ? iconPather.iconPath(this, material) : null; } - /// Signals to vanilla and Forge's item-rendering dispatch (`RenderItem`, `ItemRenderer`) that this block's item - /// form has translucent content whenever it [#hasBaseTexture] -- those callers enable alpha blending around the - /// composite draw precisely when this returns nonzero, and [ShapeBlockRenderingHandler]'s single world draw - /// participates in exactly the render pass this names ([net.minecraft.block.Block#canRenderInPass] defaults to - /// `pass == getRenderBlockPass()`), so the overlay's transparent pixels are blended, not just alpha-tested. - @Override - @SideOnly(Side.CLIENT) - public int getRenderBlockPass() { return baseTexture != null ? 1 : 0; } - @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java index 3536a802..3e74cd98 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java @@ -28,12 +28,15 @@ /// would; this is the same technique GT5-Unofficial's `gregtech.common.render.GTRendererBlock` uses for its own /// texture-array composites. /// -/// [ShapeBlock#getRenderBlockPass] returns 1 for a block [ShapeBlock#hasBaseTexture], both to keep this handler's -/// single [#renderWorldBlock] call inside the alpha-blended chunk pass (`Block#canRenderInPass` defaults to -/// `pass == getRenderBlockPass()`, so a block renders in exactly one pass without any override here) and because -/// vanilla and Forge's item-rendering dispatch (`RenderItem`, `ItemRenderer`) read that same value to decide whether -/// to enable alpha blending around a block-rendered item icon -- without it the overlay's transparent pixels would -/// only be alpha-tested, not blended. +/// Composite blocks keep the vanilla render-pass defaults (`getRenderBlockPass` 0, `canRenderInPass` only pass 0), +/// so the whole composite -- opaque base included -- lives in the solid chunk pass, where the alpha test cuts out +/// the overlay's transparent pixels the same way it did for legacy GT ores (whose composite quads all draw in pass +/// 0 too: every GT `ITexture` gates itself by its `IIconContainer#canRenderInPass`, default pass 0). The overlay +/// icons are cutout textures, not translucent ones, so the blended pass buys nothing; an opaque cube there would +/// instead sort against genuine translucents (water, glass) and pay for translucency sorting in Angelica's mesher. +/// Item contexts likewise cut out via the alpha test alone: `RenderItem` and `ItemRenderer` only enable blending +/// for a block whose `getRenderBlockPass` is nonzero, and the default 0 gives the composite the same alpha-tested +/// draw legacy ore items always had. /// /// This handler holds no mutable state, so one instance is safe to reuse from any thread a world mesher (e.g. /// Angelica's Celeritas) calls it from. From ffd5c9b83b16c4ba81f262c90d55f19a346dfea1 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sat, 11 Jul 2026 15:02:38 -0500 Subject: [PATCH 11/18] Add a fluid-specific tint override and an existence-checked fluid icon fallback --- .../materiallib/api/MaterialBuilder.java | 6 +++ .../ruling_0/materiallib/api/ShapeFluid.java | 37 ++++++++++++++++--- .../api/ShapeFluidInContainer.java | 6 ++- .../materiallib/api/StandardProperties.java | 8 ++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java b/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java index 26e6eebb..8d5d4e32 100644 --- a/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java +++ b/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java @@ -36,6 +36,12 @@ public MaterialBuilder setTint(int tint) { return setProperty(StandardProperties.TINT, tint); } + /// Sets [StandardProperties#FLUID_TINT], the ARGB tint applied to a fluid shape's fill icon in place of + /// [#setTint]. + public MaterialBuilder setFluidTint(int fluidTint) { + return setProperty(StandardProperties.FLUID_TINT, fluidTint); + } + /// Sets a property value. Rejects [StandardProperties#NAME] and [StandardProperties#TEXTURE_SET]. public MaterialBuilder setProperty(Property property, T value) { StandardProperties.requireSettable(property, value); diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java index 3105de07..573a5636 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java @@ -9,12 +9,14 @@ import net.minecraftforge.fluids.FluidRegistry; import net.minecraftforge.fluids.FluidStack; +import com.gtnewhorizon.gtnhlib.util.ResourceUtil; import com.ruling_0.materiallib.MaterialLib; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; /// The Forge fluid backing a fluid [Shape]: one registered [Fluid] per material that generates the shape, named by /// this shape's [FluidNamer] (by default `.` lowercased, e.g. `test.testiron`). @@ -27,8 +29,8 @@ /// /// A bare fluid has no item form, so its material tooltip is carried by its container item (see /// [ShapeFluidInContainer]). Each fluid takes its display name from the shape's format and its color from the -/// material's [StandardProperties#TINT], so tank and GUI renderers show the right name and tint without a custom -/// fluid block. +/// material's [StandardProperties#FLUID_TINT], or [StandardProperties#TINT] when unset, so tank and GUI renderers +/// show the right name and tint without a custom fluid block. public class ShapeFluid implements ServedShape { private static final List NO_OREDICTS = List.of(); @@ -44,6 +46,7 @@ public class ShapeFluid implements ServedShape { private final ServedMaterials served = new ServedMaterials(); private final Int2ObjectMap fluidsByIndex = new Int2ObjectOpenHashMap<>(); + private final Set warnedMissingIcon = new ReferenceOpenHashSet<>(); /// Creates a fluid shape. `displayNameFormat` is applied to the material name to build the fluid's display /// name, e.g. `"Molten %s"`. Identifiers must be non-empty and free of ':' and whitespace. @@ -172,14 +175,27 @@ FluidStack fluidStack(Material material, int amount) { return new FluidStack(fluid, amount); } - /// Binds each material's still and flowing fluid icon from [#iconPath]. Fluid textures live on the block - /// atlas, so this runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). + /// Binds each material's still and flowing fluid icon from [#iconPath], or the [ShapeIcons#EMPTY_ICON] + /// placeholder -- logged once per material -- if that path names no existing texture file, the same + /// existence-checked fallback [ShapeIcons] uses for a block or item shape's icon. Fluid textures live on the + /// block atlas, so this runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). @SideOnly(Side.CLIENT) void registerIcons(IIconRegister register) { for (Material material : served.get()) { Fluid fluid = fluidsByIndex.get(material.getIndex()); if (!(fluid instanceof MaterialFluid)) continue; - fluid.setIcons(register.registerIcon(iconPath(material))); + String path = iconPath(material); + if (!ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(path))) { + if (warnedMissingIcon.add(material)) { + MaterialLib.LOG.warn( + "Fluid shape {} of {} has no icon at {}; it will render the empty placeholder instead", + this, + material.getKey(), + path); + } + path = ShapeIcons.EMPTY_ICON; + } + fluid.setIcons(register.registerIcon(path)); } } @@ -214,6 +230,15 @@ public String getLocalizedName(FluidStack stack) { } @Override - public int getColor() { return material.getProperty(StandardProperties.TINT) & 0xFFFFFF; } + public int getColor() { return tintOf(material) & 0xFFFFFF; } + } + + /// The ARGB fill tint for `material`'s fluid: [StandardProperties#FLUID_TINT] when set, or + /// [StandardProperties#TINT] otherwise. Shared with [ShapeFluidInContainer] so a container's fill layer + /// matches the fluid it holds; a caller reading a fluid (rather than an item) color masks off the alpha byte + /// itself, as [Fluid#getColor] expects (see [MaterialFluid#getColor]). + static int tintOf(Material material) { + Integer fluidTint = material.getProperty(StandardProperties.FLUID_TINT); + return fluidTint != null ? fluidTint : material.getProperty(StandardProperties.TINT); } } diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java index f3d9a294..8423d706 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java @@ -143,11 +143,13 @@ public IIcon getIconFromDamage(int damage) { return emptyIcon; } - /// White for the untinted container base in pass 0, and the material tint -- [ShapeItem]'s pass-0 color -- for + /// White for the untinted container base in pass 0, and the fluid fill tint (see [ShapeFluid#tintOf]) for /// every later pass. @Override @SideOnly(Side.CLIENT) public int getColorFromItemStack(ItemStack stack, int renderPass) { - return renderPass == 0 ? 0xFFFFFFFF : super.getColorFromItemStack(stack, 0); + if (renderPass == 0) return 0xFFFFFFFF; + Material material = ShapeText.materialFor(stack); + return material != null ? ShapeFluid.tintOf(material) : 0xFFFFFFFF; } } diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index 8cec44a8..a9bb23cb 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -26,6 +26,14 @@ private StandardProperties() {} /// ARGB tint applied to the material's textures. public static final Property TINT = Property.of(MaterialLib.MODID, "tint", 0xFFFFFFFF); + /// ARGB tint applied to a fluid shape's fill icon in place of [#TINT], for a fluid whose art already encodes + /// its color (e.g. dedicated, hand-drawn fluid art) and so must not also receive the material's general tint. + /// Optional and null by default, like [#FALLBACK_TEXTURE_SET]: most materials never set this, and [ShapeFluid]/ + /// [ShapeFluidInContainer] fall back to [#TINT] when it is unset. Applies only to the fluid fill layer (and the + /// matching fill layer of any fluid-in-container shape holding it) -- every other shape a material generates + /// (dust, ingot, block, etc.) always uses [#TINT]. + public static final Property FLUID_TINT = Property.of(MaterialLib.MODID, "fluidTint"); + /// Rejects the properties derived from builder arguments, which can never be set or removed directly. static void requireSettable(Property property) { if (property == NAME || property == TEXTURE_SET) { From ff3db23c8969e19d0b36362a897e6bf544756e05 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:51:31 -0500 Subject: [PATCH 12/18] Add BLOCK_OVERLAY_TINT to let a block shape's overlay layer skip TINT --- .../com/ruling_0/materiallib/api/ShapeBlock.java | 16 ++++++++++++---- .../materiallib/api/StandardProperties.java | 9 +++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 327e1c3c..be991864 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -258,11 +258,19 @@ public int colorMultiplier(IBlockAccess world, int x, int y, int z) { } /// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material. - /// Block render colors carry no alpha, so the material's ARGB [StandardProperties#TINT] is masked to its low - /// 24 bits. - private static int tintFor(int meta) { + /// Block render colors carry no alpha, so the resolved ARGB value is masked to its low 24 bits. For a + /// [#hasBaseTexture] composite, this resolves the overlay layer's tint: [StandardProperties#BLOCK_OVERLAY_TINT] + /// when the material sets it, or [StandardProperties#TINT] otherwise, the same fallback [ShapeFluid] uses for + /// [StandardProperties#FLUID_TINT]. A plain block shape with no base texture -- e.g. a material's compressed + /// storage block -- has no overlay layer to speak of and always uses [StandardProperties#TINT] directly. + private int tintFor(int meta) { Material material = MaterialRegistry.instance().getMaterialByIndex(meta); - return material != null ? material.getProperty(StandardProperties.TINT) & 0xFFFFFF : 0xFFFFFF; + if (material == null) return 0xFFFFFF; + if (baseTexture != null) { + Integer overlayTint = material.getProperty(StandardProperties.BLOCK_OVERLAY_TINT); + if (overlayTint != null) return overlayTint & 0xFFFFFF; + } + return material.getProperty(StandardProperties.TINT) & 0xFFFFFF; } @Override diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index a9bb23cb..69c6b3ed 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -34,6 +34,15 @@ private StandardProperties() {} /// (dust, ingot, block, etc.) always uses [#TINT]. public static final Property FLUID_TINT = Property.of(MaterialLib.MODID, "fluidTint"); + /// ARGB tint applied to a [ShapeBlock#hasBaseTexture] block shape's overlay icon layer in place of [#TINT], + /// for a material whose overlay art already encodes its own color (e.g. hand-painted ore splotches carried + /// over from a pre-tinted legacy icon) and so must not also receive the material's general tint. Optional and + /// null by default, like [#FLUID_TINT]: most materials never set this, and [ShapeBlock] falls back to [#TINT] + /// when it is unset. Applies only to a base-textured block shape's overlay layer (e.g. `ore`/`oreSmall`'s + /// tinted icon drawn over their untinted per-variant stone background) -- a block shape with no base texture, + /// such as a material's compressed storage block, always uses [#TINT] directly. + public static final Property BLOCK_OVERLAY_TINT = Property.of(MaterialLib.MODID, "blockOverlayTint"); + /// Rejects the properties derived from builder arguments, which can never be set or removed directly. static void requireSettable(Property property) { if (property == NAME || property == TEXTURE_SET) { From acc8628ab619cb90e6ddbd731e740a2e6a2e34b3 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:22:38 -0500 Subject: [PATCH 13/18] Add BLOCK_TINT to let a plain block shape's icon skip TINT --- .../java/com/ruling_0/materiallib/api/ShapeBlock.java | 10 +++++----- .../ruling_0/materiallib/api/StandardProperties.java | 8 ++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index be991864..b545abc1 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -262,14 +262,14 @@ public int colorMultiplier(IBlockAccess world, int x, int y, int z) { /// [#hasBaseTexture] composite, this resolves the overlay layer's tint: [StandardProperties#BLOCK_OVERLAY_TINT] /// when the material sets it, or [StandardProperties#TINT] otherwise, the same fallback [ShapeFluid] uses for /// [StandardProperties#FLUID_TINT]. A plain block shape with no base texture -- e.g. a material's compressed - /// storage block -- has no overlay layer to speak of and always uses [StandardProperties#TINT] directly. + /// storage block -- has no overlay layer to speak of and resolves [StandardProperties#BLOCK_TINT] when the + /// material sets it, or [StandardProperties#TINT] otherwise. private int tintFor(int meta) { Material material = MaterialRegistry.instance().getMaterialByIndex(meta); if (material == null) return 0xFFFFFF; - if (baseTexture != null) { - Integer overlayTint = material.getProperty(StandardProperties.BLOCK_OVERLAY_TINT); - if (overlayTint != null) return overlayTint & 0xFFFFFF; - } + Integer override = material.getProperty( + baseTexture != null ? StandardProperties.BLOCK_OVERLAY_TINT : StandardProperties.BLOCK_TINT); + if (override != null) return override & 0xFFFFFF; return material.getProperty(StandardProperties.TINT) & 0xFFFFFF; } diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index 69c6b3ed..df64af5e 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -43,6 +43,14 @@ private StandardProperties() {} /// such as a material's compressed storage block, always uses [#TINT] directly. public static final Property BLOCK_OVERLAY_TINT = Property.of(MaterialLib.MODID, "blockOverlayTint"); + /// ARGB tint applied to a plain (no-base-texture) block shape's icon in place of [#TINT], for a material + /// whose whole-block art already encodes its own color (e.g. a dedicated pre-colored storage-block texture) + /// and so must not also receive the material's general tint. Optional and null by default, like + /// [#FLUID_TINT]: most materials never set this, and [ShapeBlock] falls back to [#TINT] when it is unset. + /// The plain-block counterpart of [#BLOCK_OVERLAY_TINT], which covers only the overlay layer of a + /// base-textured composite; a composite block shape never consults this property. + public static final Property BLOCK_TINT = Property.of(MaterialLib.MODID, "blockTint"); + /// Rejects the properties derived from builder arguments, which can never be set or removed directly. static void requireSettable(Property property) { if (property == NAME || property == TEXTURE_SET) { From cf293de41065396190fa40ad4aaf6f466f94eee0 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Sun, 12 Jul 2026 03:58:05 -0500 Subject: [PATCH 14/18] Add CELL_TINT to let a container's fill layer skip FLUID_TINT --- .../java/com/ruling_0/materiallib/api/ShapeFluid.java | 6 +++--- .../ruling_0/materiallib/api/ShapeFluidInContainer.java | 9 ++++++--- .../com/ruling_0/materiallib/api/StandardProperties.java | 8 ++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java index 573a5636..c22f51cb 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java @@ -234,9 +234,9 @@ public String getLocalizedName(FluidStack stack) { } /// The ARGB fill tint for `material`'s fluid: [StandardProperties#FLUID_TINT] when set, or - /// [StandardProperties#TINT] otherwise. Shared with [ShapeFluidInContainer] so a container's fill layer - /// matches the fluid it holds; a caller reading a fluid (rather than an item) color masks off the alpha byte - /// itself, as [Fluid#getColor] expects (see [MaterialFluid#getColor]). + /// [StandardProperties#TINT] otherwise. [ShapeFluidInContainer] falls back to this for its own fill layer when + /// the material sets no [StandardProperties#CELL_TINT]; a caller reading a fluid (rather than an item) color + /// masks off the alpha byte itself, as [Fluid#getColor] expects (see [MaterialFluid#getColor]). static int tintOf(Material material) { Integer fluidTint = material.getProperty(StandardProperties.FLUID_TINT); return fluidTint != null ? fluidTint : material.getProperty(StandardProperties.TINT); diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java index 8423d706..a779ef85 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java @@ -143,13 +143,16 @@ public IIcon getIconFromDamage(int damage) { return emptyIcon; } - /// White for the untinted container base in pass 0, and the fluid fill tint (see [ShapeFluid#tintOf]) for - /// every later pass. + /// White for the untinted container base in pass 0, and the fill tint for every later pass: + /// [StandardProperties#CELL_TINT] when the material sets it, or the fluid fill tint (see [ShapeFluid#tintOf]) + /// otherwise. @Override @SideOnly(Side.CLIENT) public int getColorFromItemStack(ItemStack stack, int renderPass) { if (renderPass == 0) return 0xFFFFFFFF; Material material = ShapeText.materialFor(stack); - return material != null ? ShapeFluid.tintOf(material) : 0xFFFFFFFF; + if (material == null) return 0xFFFFFFFF; + Integer cellTint = material.getProperty(StandardProperties.CELL_TINT); + return cellTint != null ? cellTint : ShapeFluid.tintOf(material); } } diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index df64af5e..9f36c7cf 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -51,6 +51,14 @@ private StandardProperties() {} /// base-textured composite; a composite block shape never consults this property. public static final Property BLOCK_TINT = Property.of(MaterialLib.MODID, "blockTint"); + /// ARGB tint applied to a [ShapeFluidInContainer]'s fill layer in place of [#FLUID_TINT], for a material whose + /// fluid renders one color in the world (e.g. untinted over dedicated art) but whose legacy cell fill was + /// tinted differently. Optional and null by default, like [#FLUID_TINT]: most materials never set this, and + /// [ShapeFluidInContainer] falls back to [#FLUID_TINT], then [#TINT], when it is unset. Applies only to a + /// container's fill layer -- the fluid's own world/tank/GUI rendering (see [ShapeFluid]) always uses + /// [#FLUID_TINT], never this property. + public static final Property CELL_TINT = Property.of(MaterialLib.MODID, "cellTint"); + /// Rejects the properties derived from builder arguments, which can never be set or removed directly. static void requireSettable(Property property) { if (property == NAME || property == TEXTURE_SET) { From 7e7b88c051562a60c03ddca373af797966a355bd Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:02:03 -0500 Subject: [PATCH 15/18] Trim block rendering docs to contract statements --- .../materiallib/api/BlockShapeBuilder.java | 10 ++-- .../ruling_0/materiallib/api/ShapeBlock.java | 46 ++++++------------- .../api/ShapeBlockRenderingHandler.java | 35 ++++---------- .../ruling_0/materiallib/api/ShapeFluid.java | 11 ++--- .../materiallib/api/StandardProperties.java | 37 +++++---------- .../materiallib/examples/ExampleContent.java | 9 +--- .../api/ResourceUtilBaseTexturePathTest.java | 9 +--- .../materiallib/api/ShapeBlockTest.java | 17 ++----- .../examples/ExampleContentTest.java | 8 +--- 9 files changed, 52 insertions(+), 130 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java index 4777acab..251eb79a 100644 --- a/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java +++ b/src/main/java/com/ruling_0/materiallib/api/BlockShapeBuilder.java @@ -61,12 +61,10 @@ public BlockShapeBuilder variants(String... variants) { /// Declares the untinted background texture drawn under `variant`'s tinted material icon (e.g. the stone /// background of an ore), as a `domain:path` icon identifier (`"minecraft:stone"`) independent of any - /// material's texture set -- the same convention [TextureSet#iconPath] uses, resolved the same way; do not - /// include the `blocks/` folder, it is implicit (see - /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]). `texture` is - /// composited under the tinted material icon by [ShapeBlockRenderingHandler]; see [ShapeBlock#hasBaseTexture]. - /// Optional -- a variant with no base texture renders as a single tinted layer. `variant` must be one of the - /// names passed to [#variants]. + /// material's texture set; the `blocks/` folder is implicit (see + /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]). + /// [ShapeBlockRenderingHandler] composites the two layers. Optional -- a variant with no base texture renders + /// as a single tinted layer. `variant` must be one of the names passed to [#variants]. public BlockShapeBuilder variantBase(String variant, String texture) { Objects.requireNonNull(variant, "variant must not be null"); if (texture == null || texture.isEmpty()) { diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index b545abc1..04a65d85 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -36,12 +36,10 @@ /// /// A variant block built by [ShapeBlockVariants] additionally falls back from its own icon (`_`) /// to the plain shape name, and may draw an untinted base texture (e.g. a stone background) under the tinted -/// material icon; see [#registerBlockIcons]. [ShapeBlockRenderingHandler] composites both layers -- in world and in -/// every item form (GUI slot, hotbar, held, and dropped) -- in a single draw per block, driven by [#getIcon], -/// [#getRenderColor], and [#colorMultiplier] through [#layerOverride]; see [#hasBaseTexture]. Drops, hardness, -/// resistance, and harvest level may be overridden per material and variant, and the harvest tool class per -/// shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla default it -/// replaces. +/// material icon, composited by [ShapeBlockRenderingHandler]; see [#registerBlockIcons] and [#hasBaseTexture]. +/// Drops, hardness, resistance, and harvest level may be overridden per material and variant, and the harvest tool +/// class per shape, through [BlockShapeBuilder]'s behavior hooks; a hook left unset preserves the vanilla default +/// it replaces. public class ShapeBlock extends Block implements BackedShape { private final String modid; @@ -175,12 +173,7 @@ public void registerBlockIcons(IIconRegister register) { } /// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder -- logged - /// once -- if it does not, the same existence-checked fallback [ShapeIcons] uses for a material's texture-set - /// icon. Without this check a bad base texture path renders as Minecraft's own unlogged missing-texture - /// checkerboard instead of a diagnosable warning. The existence check and the registration both resolve - /// [#baseTexture] itself -- never a derived or re-formatted copy of it -- so they always agree; see - /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation] for how that single - /// string maps to the file the check looks for. + /// once -- if it does not. private IIcon registerBaseIcon(IIconRegister register) { if (ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(baseTexture))) { return register.registerIcon(baseTexture); @@ -197,30 +190,20 @@ private IIcon registerBaseIcon(IIconRegister register) { } /// Whether this variant draws a base texture layer under the tinted material icon; see [#registerBlockIcons]. - /// [ClientProxy][com.ruling_0.materiallib.ClientProxy] uses this to decide which block shapes need - /// [#setRenderType] pointed at [ShapeBlockRenderingHandler] -- a plain block shape has nothing to composite and - /// keeps the vanilla full-cube render type (0). public boolean hasBaseTexture() { return baseTexture != null; } - /// This block's [#getRenderType]; [ClientProxy][com.ruling_0.materiallib.ClientProxy] sets this once, at - /// preInit, to [ShapeBlockRenderingHandler]'s render ID for every block [#hasBaseTexture]. Never called on the - /// dedicated server, where [ShapeBlockRenderingHandler] itself must never classload. + /// Sets the render type [#getRenderType] reports: [ShapeBlockRenderingHandler]'s render ID for a + /// [#hasBaseTexture] composite, or the vanilla full-cube default (0). Client only. public void setRenderType(int renderType) { this.renderType = renderType; } @Override public int getRenderType() { return renderType; } /// Forces [#getIcon], [#getRenderColor], and [#colorMultiplier] to resolve the base layer (0) or the tinted - /// overlay layer (1) regardless of metadata, or -1 to resolve the base layer by default. - /// [ShapeBlockRenderingHandler] toggles this around each of the two draws its single [#hasBaseTexture] - /// composite needs -- both the world and the item-form (GUI slot, hotbar, held, and dropped) renderers set it - /// explicitly before every draw, so -1 is only reached by other code that calls [#getIcon] or [#getRenderColor] - /// directly, e.g. a mod inspecting an icon outside `RenderItem` entirely. The untinted base is a recognizable - /// icon in that case, where the tinted overlay alone renders as a transparent slot with a few floating tinted - /// flecks, since the overlay icon is a sparse, mostly-transparent layer meant to be drawn over the base, never - /// standalone. + /// overlay layer (1). The resting value -1 resolves the base layer, the only one recognizable on its own for a + /// caller reading icons outside [ShapeBlockRenderingHandler]'s draws. void setLayerOverride(int layer) { layerOverride = layer; } /// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight @@ -257,13 +240,10 @@ public int colorMultiplier(IBlockAccess world, int x, int y, int z) { return tintFor(world.getBlockMetadata(x, y, z)); } - /// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material. - /// Block render colors carry no alpha, so the resolved ARGB value is masked to its low 24 bits. For a - /// [#hasBaseTexture] composite, this resolves the overlay layer's tint: [StandardProperties#BLOCK_OVERLAY_TINT] - /// when the material sets it, or [StandardProperties#TINT] otherwise, the same fallback [ShapeFluid] uses for - /// [StandardProperties#FLUID_TINT]. A plain block shape with no base texture -- e.g. a material's compressed - /// storage block -- has no overlay layer to speak of and resolves [StandardProperties#BLOCK_TINT] when the - /// material sets it, or [StandardProperties#TINT] otherwise. + /// The RGB tint of the material at the given metadata, or white when the metadata maps to no live material: + /// [StandardProperties#BLOCK_OVERLAY_TINT] for a [#hasBaseTexture] composite's overlay layer, + /// [StandardProperties#BLOCK_TINT] for a plain block, [StandardProperties#TINT] when the specific property is + /// unset. Block render colors carry no alpha, so the resolved ARGB value is masked to its low 24 bits. private int tintFor(int meta) { Material material = MaterialRegistry.instance().getMaterialByIndex(meta); if (material == null) return 0xFFFFFF; diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java index 3e74cd98..d5c329b0 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java @@ -13,33 +13,18 @@ import org.lwjgl.opengl.GL11; /// Renders a [ShapeBlock#hasBaseTexture] composite -- an untinted base texture under a tinted material icon -- as a -/// single draw, in world and in every item form (GUI slot, hotbar, held, and dropped). -/// [com.ruling_0.materiallib.ClientProxy] -/// registers one instance of this handler for every such block, through [#RENDER_ID] and [ShapeBlock#setRenderType]; -/// a block with no base texture keeps the vanilla full-cube render type (0) and never reaches this class. +/// single draw, in world and in every item form (GUI slot, hotbar, held, and dropped). A block with no base +/// texture keeps the vanilla full-cube render type and never reaches this handler; see [ShapeBlock#setRenderType]. /// -/// Both [#renderWorldBlock] and [#renderInventoryBlock] draw the base layer and the tinted overlay back-to-back -/// into the same [Tessellator] batch, toggling [ShapeBlock#setLayerOverride] between the two so [ShapeBlock#getIcon], -/// [ShapeBlock#getRenderColor], and [ShapeBlock#colorMultiplier] resolve the base layer for the first draw and the -/// overlay for the second -- the composite that used to take two separate render passes (world chunk tessellation's -/// solid and alpha passes, or two `renderBlockAsItem` calls) now happens in one. Because the two layers share a -/// tessellator batch, their coplanar quads are submitted back-to-back with identical vertex data, so the depth test -/// resolves the tie deterministically in submission order instead of z-fighting the way two independent draw calls -/// would; this is the same technique GT5-Unofficial's `gregtech.common.render.GTRendererBlock` uses for its own -/// texture-array composites. +/// Both [#renderWorldBlock] and [#renderInventoryBlock] draw the two layers back-to-back into the same +/// [Tessellator] batch, toggling [ShapeBlock#setLayerOverride] so [ShapeBlock#getIcon], +/// [ShapeBlock#getRenderColor], and [ShapeBlock#colorMultiplier] resolve the base for the first draw and the +/// overlay for the second. Sharing one batch submits the coplanar quads with identical vertex data, so the depth +/// test resolves the tie in submission order instead of z-fighting. /// -/// Composite blocks keep the vanilla render-pass defaults (`getRenderBlockPass` 0, `canRenderInPass` only pass 0), -/// so the whole composite -- opaque base included -- lives in the solid chunk pass, where the alpha test cuts out -/// the overlay's transparent pixels the same way it did for legacy GT ores (whose composite quads all draw in pass -/// 0 too: every GT `ITexture` gates itself by its `IIconContainer#canRenderInPass`, default pass 0). The overlay -/// icons are cutout textures, not translucent ones, so the blended pass buys nothing; an opaque cube there would -/// instead sort against genuine translucents (water, glass) and pay for translucency sorting in Angelica's mesher. -/// Item contexts likewise cut out via the alpha test alone: `RenderItem` and `ItemRenderer` only enable blending -/// for a block whose `getRenderBlockPass` is nonzero, and the default 0 gives the composite the same alpha-tested -/// draw legacy ore items always had. -/// -/// This handler holds no mutable state, so one instance is safe to reuse from any thread a world mesher (e.g. -/// Angelica's Celeritas) calls it from. +/// The whole composite draws in the solid chunk pass (the vanilla render-pass defaults), where the alpha test cuts +/// out the overlay's transparent pixels; the overlay icons are cutout textures, not translucent ones, matching +/// legacy GT ore blocks. @SideOnly(Side.CLIENT) public final class ShapeBlockRenderingHandler implements ISimpleBlockRenderingHandler { diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java index c22f51cb..5786ed64 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java @@ -176,9 +176,8 @@ FluidStack fluidStack(Material material, int amount) { } /// Binds each material's still and flowing fluid icon from [#iconPath], or the [ShapeIcons#EMPTY_ICON] - /// placeholder -- logged once per material -- if that path names no existing texture file, the same - /// existence-checked fallback [ShapeIcons] uses for a block or item shape's icon. Fluid textures live on the - /// block atlas, so this runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). + /// placeholder -- logged once per material -- if that path names no existing texture file. Fluid textures live + /// on the block atlas, so this runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). @SideOnly(Side.CLIENT) void registerIcons(IIconRegister register) { for (Material material : served.get()) { @@ -212,7 +211,7 @@ String iconPath(Material material) { } /// A material's fluid, naming itself from the shape's display format and coloring itself with the material's - /// [StandardProperties#TINT] so renderers that read [Fluid#getColor] tint the fluid per material. 1.7.10 fluids + /// fill tint (see [#tintOf]) so renderers that read [Fluid#getColor] tint the fluid per material. 1.7.10 fluids /// expose no color setter, only an overridable [Fluid#getColor], which is why this is a subclass. private final class MaterialFluid extends Fluid { @@ -234,9 +233,7 @@ public String getLocalizedName(FluidStack stack) { } /// The ARGB fill tint for `material`'s fluid: [StandardProperties#FLUID_TINT] when set, or - /// [StandardProperties#TINT] otherwise. [ShapeFluidInContainer] falls back to this for its own fill layer when - /// the material sets no [StandardProperties#CELL_TINT]; a caller reading a fluid (rather than an item) color - /// masks off the alpha byte itself, as [Fluid#getColor] expects (see [MaterialFluid#getColor]). + /// [StandardProperties#TINT] otherwise. static int tintOf(Material material) { Integer fluidTint = material.getProperty(StandardProperties.FLUID_TINT); return fluidTint != null ? fluidTint : material.getProperty(StandardProperties.TINT); diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index 9f36c7cf..bf7f1a02 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -26,37 +26,24 @@ private StandardProperties() {} /// ARGB tint applied to the material's textures. public static final Property TINT = Property.of(MaterialLib.MODID, "tint", 0xFFFFFFFF); - /// ARGB tint applied to a fluid shape's fill icon in place of [#TINT], for a fluid whose art already encodes - /// its color (e.g. dedicated, hand-drawn fluid art) and so must not also receive the material's general tint. - /// Optional and null by default, like [#FALLBACK_TEXTURE_SET]: most materials never set this, and [ShapeFluid]/ - /// [ShapeFluidInContainer] fall back to [#TINT] when it is unset. Applies only to the fluid fill layer (and the - /// matching fill layer of any fluid-in-container shape holding it) -- every other shape a material generates - /// (dust, ingot, block, etc.) always uses [#TINT]. + /// ARGB tint applied to a fluid shape's fill icon in place of [#TINT], for fluid art that already encodes its + /// color. Null when unset, falling back to [#TINT]. Applies only to the fluid fill layer; every other shape a + /// material generates uses [#TINT]. public static final Property FLUID_TINT = Property.of(MaterialLib.MODID, "fluidTint"); - /// ARGB tint applied to a [ShapeBlock#hasBaseTexture] block shape's overlay icon layer in place of [#TINT], - /// for a material whose overlay art already encodes its own color (e.g. hand-painted ore splotches carried - /// over from a pre-tinted legacy icon) and so must not also receive the material's general tint. Optional and - /// null by default, like [#FLUID_TINT]: most materials never set this, and [ShapeBlock] falls back to [#TINT] - /// when it is unset. Applies only to a base-textured block shape's overlay layer (e.g. `ore`/`oreSmall`'s - /// tinted icon drawn over their untinted per-variant stone background) -- a block shape with no base texture, - /// such as a material's compressed storage block, always uses [#TINT] directly. + /// ARGB tint applied to a [ShapeBlock#hasBaseTexture] composite's overlay icon layer in place of [#TINT], for + /// overlay art that already encodes its color. Null when unset, falling back to [#TINT]. A block shape with no + /// base texture consults [#BLOCK_TINT] instead. public static final Property BLOCK_OVERLAY_TINT = Property.of(MaterialLib.MODID, "blockOverlayTint"); - /// ARGB tint applied to a plain (no-base-texture) block shape's icon in place of [#TINT], for a material - /// whose whole-block art already encodes its own color (e.g. a dedicated pre-colored storage-block texture) - /// and so must not also receive the material's general tint. Optional and null by default, like - /// [#FLUID_TINT]: most materials never set this, and [ShapeBlock] falls back to [#TINT] when it is unset. - /// The plain-block counterpart of [#BLOCK_OVERLAY_TINT], which covers only the overlay layer of a - /// base-textured composite; a composite block shape never consults this property. + /// ARGB tint applied to a plain (no-base-texture) block shape's icon in place of [#TINT], for whole-block art + /// that already encodes its color. Null when unset, falling back to [#TINT]. A [ShapeBlock#hasBaseTexture] + /// composite consults [#BLOCK_OVERLAY_TINT] instead. public static final Property BLOCK_TINT = Property.of(MaterialLib.MODID, "blockTint"); - /// ARGB tint applied to a [ShapeFluidInContainer]'s fill layer in place of [#FLUID_TINT], for a material whose - /// fluid renders one color in the world (e.g. untinted over dedicated art) but whose legacy cell fill was - /// tinted differently. Optional and null by default, like [#FLUID_TINT]: most materials never set this, and - /// [ShapeFluidInContainer] falls back to [#FLUID_TINT], then [#TINT], when it is unset. Applies only to a - /// container's fill layer -- the fluid's own world/tank/GUI rendering (see [ShapeFluid]) always uses - /// [#FLUID_TINT], never this property. + /// ARGB tint applied to a [ShapeFluidInContainer]'s fill layer in place of the fluid fill tint, for a legacy + /// cell fill tinted differently than the fluid itself. Null when unset, falling back to [#FLUID_TINT], then + /// [#TINT]. The fluid's own rendering (see [ShapeFluid]) never consults this property. public static final Property CELL_TINT = Property.of(MaterialLib.MODID, "cellTint"); /// Rejects the properties derived from builder arguments, which can never be set or removed directly. diff --git a/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java b/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java index 09fe6ae5..2599291a 100644 --- a/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java +++ b/src/main/java/com/ruling_0/materiallib/examples/ExampleContent.java @@ -28,13 +28,8 @@ /// TestGold show per-pair display names. public final class ExampleContent { - /// The base texture testOre's `stone` variant draws under its tinted material icon; see [#register]. Package - /// visible so [ExampleContentTest][com.ruling_0.materiallib.examples.ExampleContentTest] pins it against - /// [com.gtnewhorizon.gtnhlib.util.ResourceUtil#getCompleteBlockTextureResourceLocation]'s convention -- a - /// path with no `blocks/` segment, since that prefix is implicit in the base path the resource-location lookup - /// already applies, naming a file that actually ships in this Minecraft version's vanilla jar. A doubled - /// `blocks/` segment, or a texture 1.7.10 never shipped (granite/diorite/andesite stone variants are a 1.8+ - /// feature), resolves to a nonexistent file and silently falls back to the transparent placeholder icon. + /// The base texture testOre's `stone` variant draws under its tinted material icon. Must name a texture the + /// 1.7.10 vanilla jar ships, with no `blocks/` segment (that folder is implicit in block icon identifiers). static final String TEST_ORE_STONE_BASE_TEXTURE = "minecraft:stone"; /// As [#TEST_ORE_STONE_BASE_TEXTURE], for testOre's `cobblestone` variant. diff --git a/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java b/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java index 624aa561..f8a11236 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java @@ -8,13 +8,8 @@ import org.junit.jupiter.api.Test; -/// Pins [ResourceUtil]'s block-texture path construction, the same construction -/// [ShapeBlock#registerBaseIcon] uses both to existence-check a variant's base texture and, on success, to -/// register it -- independent of any live Minecraft resource manager (see [ShapeBlockTest]'s javadoc for why the -/// existence check itself still needs a live client). A wrong construction here would make a real texture -/// (`minecraft:stone`) register as if it were missing, or send a mixed-case cross-mod domain -/// (`GalacticraftCore:moon`) looking under the wrong domain; 1.7.10 resource lookups are case-sensitive against -/// the packed jar entries a resource pack actually declares. +/// Pins the block-texture path construction [ShapeBlock]'s base-texture existence check and registration both +/// resolve through. 1.7.10 resource lookups are case-sensitive, so a mixed-case domain must survive intact. class ResourceUtilBaseTexturePathTest { @Test diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index 1893e868..e215c5d2 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -7,13 +7,9 @@ import org.junit.jupiter.api.Test; /// Headless coverage for [ShapeBlock]'s base-texture and render-type bookkeeping. [ShapeBlock#getRenderColor]'s -/// tinted branch resolves a material through the process-wide [MaterialRegistry#instance], which is unresolved -/// (and throws on lookup) until MaterialLib's preInit runs, so no test here reaches it; only the untinted (layer 0) -/// branch, which returns before consulting any material, is exercised. -/// -/// World compositing, item-form compositing, and [ShapeBlockRenderingHandler]'s registration all need a live -/// client -- the user must verify in-client that a variant ore block shows its base texture under the tinted -/// material icon, in world and in every item-form context: GUI slot, hotbar, held, and dropped. +/// tinted branch resolves a material through the process-wide [MaterialRegistry#instance], which throws on lookup +/// before MaterialLib's preInit, so only the untinted (layer 0) branch is exercised; the compositing itself needs +/// a live client. class ShapeBlockTest { private final ShapeBlock withBaseTexture = new ShapeBlock( @@ -35,9 +31,6 @@ void hasBaseTextureReflectsWhetherAVariantBaseWasDeclared() { assertFalse(withoutBaseTexture.hasBaseTexture()); } - /// [ShapeBlockRenderingHandler] drives the composite entirely through [ShapeBlock#setLayerOverride]; layer 0 - /// (the base) must render untinted whether or not a base texture exists, the same as - /// [ShapeBlock#colorMultiplier] does for world tessellation's solid layer. @Test void layerZeroIsUntintedWhenABaseTextureExists() { withBaseTexture.setLayerOverride(0); @@ -50,10 +43,6 @@ void layerZeroIsUntintedWhenABaseTextureExists() { } } - /// Pins the render-type wiring [com.ruling_0.materiallib.ClientProxy] relies on: a block defaults to the - /// vanilla full-cube render type (0) and reports whatever [ShapeBlockRenderingHandler]'s render ID - /// [ShapeBlock#setRenderType] was last given, so a future change that stopped wiring composite blocks to their - /// handler would leave them silently rendering as plain cubes instead of failing to compile. @Test void getRenderTypeReflectsWhateverWasSet() { assertEquals(0, withBaseTexture.getRenderType()); diff --git a/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java index 0cf96703..85ce9f90 100644 --- a/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java +++ b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java @@ -8,12 +8,8 @@ import org.junit.jupiter.api.Test; -/// Pins testOre's declared variant base textures against -/// [ResourceUtil#getCompleteBlockTextureResourceLocation]'s convention, independent of any live Minecraft resource -/// manager (see `ShapeBlockTest`'s javadoc for why the existence check itself still needs a live client). A -/// `blocks/` segment folded into the identifier -- e.g. `"minecraft:blocks/stone"` instead of `"minecraft:stone"` -/// -- resolves to a path no vanilla jar has (`textures/blocks/blocks/stone.png`) and silently falls back to the -/// transparent placeholder icon; this test would have failed against that regression. +/// Pins testOre's declared variant base textures to [ResourceUtil#getCompleteBlockTextureResourceLocation]'s +/// convention: no `blocks/` segment in the identifier, naming files the 1.7.10 vanilla jar ships. class ExampleContentTest { @Test From a21ef21768590b8b257e06e9375d8b1d8c368286 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:24:15 -0500 Subject: [PATCH 16/18] Drive composite block rendering through explicit per-layer icons and colors --- dependencies.gradle | 3 + .../ruling_0/materiallib/api/ShapeBlock.java | 28 ++++-- .../api/ShapeBlockRenderingHandler.java | 97 +++++++++++-------- .../materiallib/api/ShapeBlockTest.java | 10 +- 4 files changed, 81 insertions(+), 57 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 1fd2b6ce..a3fe185c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -40,6 +40,9 @@ dependencies { implementation('com.github.GTNewHorizons:Postea:1.2.5:dev') + // Only for the @ThreadSafeISBRH annotation; an unresolvable annotation class is ignored at runtime. + compileOnly('com.github.GTNewHorizons:Angelica:2.1.49:dev') { transitive = false } + // EndlessIDs required for >16 materials runtimeOnly('com.falsepattern:chunkapi-mc1.7.10:0.8.3:dev') runtimeOnly('com.falsepattern:endlessids-mc1.7.10:1.7.3:dev') diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 04a65d85..847a8b4f 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -56,7 +56,6 @@ public class ShapeBlock extends Block implements BackedShape { private final ShapeIcons icons = new ShapeIcons(false); private IIcon baseIcon; private boolean warnedMissingBaseTexture; - private int layerOverride = -1; private int renderType = 0; /// Creates a block shape backed by a [net.minecraft.block.material.Material#iron] block. `oreDicts` are the @@ -195,16 +194,24 @@ public boolean hasBaseTexture() { } /// Sets the render type [#getRenderType] reports: [ShapeBlockRenderingHandler]'s render ID for a - /// [#hasBaseTexture] composite, or the vanilla full-cube default (0). Client only. + /// [#hasBaseTexture] composite, or the vanilla full-cube default (0). + @SideOnly(Side.CLIENT) public void setRenderType(int renderType) { this.renderType = renderType; } @Override public int getRenderType() { return renderType; } - /// Forces [#getIcon], [#getRenderColor], and [#colorMultiplier] to resolve the base layer (0) or the tinted - /// overlay layer (1). The resting value -1 resolves the base layer, the only one recognizable on its own for a - /// caller reading icons outside [ShapeBlockRenderingHandler]'s draws. - void setLayerOverride(int layer) { layerOverride = layer; } + /// This variant's base texture icon, or null when it declares none. + @SideOnly(Side.CLIENT) + IIcon baseIcon() { + return baseIcon; + } + + /// The material icon bound at the given metadata; see [ShapeIcons#get]. + @SideOnly(Side.CLIENT) + IIcon materialIcon(int meta) { + return icons.get(meta); + } /// The icon path to try for `material` before this shape's texture-set candidates, or null to skip straight /// to them. The default implementation defers to this block's [BlockIconPather]. A subclass may override this @@ -216,7 +223,7 @@ protected String iconPathFor(Material material) { @Override @SideOnly(Side.CLIENT) public IIcon getIcon(int side, int meta) { - if (baseTexture != null && layerOverride != 1) { + if (baseTexture != null) { return baseIcon; } return icons.get(meta); @@ -225,7 +232,7 @@ public IIcon getIcon(int side, int meta) { @Override @SideOnly(Side.CLIENT) public int getRenderColor(int meta) { - if (baseTexture != null && layerOverride != 1) { + if (baseTexture != null) { return 0xFFFFFF; } return tintFor(meta); @@ -234,7 +241,7 @@ public int getRenderColor(int meta) { @Override @SideOnly(Side.CLIENT) public int colorMultiplier(IBlockAccess world, int x, int y, int z) { - if (baseTexture != null && layerOverride != 1) { + if (baseTexture != null) { return 0xFFFFFF; } return tintFor(world.getBlockMetadata(x, y, z)); @@ -244,7 +251,8 @@ public int colorMultiplier(IBlockAccess world, int x, int y, int z) { /// [StandardProperties#BLOCK_OVERLAY_TINT] for a [#hasBaseTexture] composite's overlay layer, /// [StandardProperties#BLOCK_TINT] for a plain block, [StandardProperties#TINT] when the specific property is /// unset. Block render colors carry no alpha, so the resolved ARGB value is masked to its low 24 bits. - private int tintFor(int meta) { + @SideOnly(Side.CLIENT) + int tintFor(int meta) { Material material = MaterialRegistry.instance().getMaterialByIndex(meta); if (material == null) return 0xFFFFFF; Integer override = material.getProperty( diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java index d5c329b0..8569fcc0 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlockRenderingHandler.java @@ -1,31 +1,36 @@ package com.ruling_0.materiallib.api; import net.minecraft.block.Block; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.EntityRenderer; import net.minecraft.client.renderer.RenderBlocks; import net.minecraft.client.renderer.Tessellator; import net.minecraft.util.IIcon; import net.minecraft.world.IBlockAccess; +import com.gtnewhorizons.angelica.api.ThreadSafeISBRH; + import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler; import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import org.lwjgl.opengl.GL11; -/// Renders a [ShapeBlock#hasBaseTexture] composite -- an untinted base texture under a tinted material icon -- as a -/// single draw, in world and in every item form (GUI slot, hotbar, held, and dropped). A block with no base -/// texture keeps the vanilla full-cube render type and never reaches this handler; see [ShapeBlock#setRenderType]. +/// Renders a [ShapeBlock#hasBaseTexture] composite -- an untinted base texture under a tinted material icon -- in +/// world and in every item form (GUI slot, hotbar, held, and dropped). A block with no base texture keeps the +/// vanilla full-cube render type and never reaches this handler; see [ShapeBlock#setRenderType]. /// -/// Both [#renderWorldBlock] and [#renderInventoryBlock] draw the two layers back-to-back into the same -/// [Tessellator] batch, toggling [ShapeBlock#setLayerOverride] so [ShapeBlock#getIcon], -/// [ShapeBlock#getRenderColor], and [ShapeBlock#colorMultiplier] resolve the base for the first draw and the -/// overlay for the second. Sharing one batch submits the coplanar quads with identical vertex data, so the depth -/// test resolves the tie in submission order instead of z-fighting. +/// Each layer is drawn with its icon and color passed in explicitly, under a [RenderBlocks] override texture, so +/// the handler holds no state and one shared instance serves every thread of Angelica's off-thread chunk meshing. +/// [#renderInventoryBlock] draws both layers back-to-back into the same [Tessellator] batch; [#renderWorldBlock] +/// makes two standard-block draws with explicit colors. Submitting the coplanar quads with identical vertex data +/// lets the depth test resolve the tie in submission order instead of z-fighting. /// /// The whole composite draws in the solid chunk pass (the vanilla render-pass defaults), where the alpha test cuts /// out the overlay's transparent pixels; the overlay icons are cutout textures, not translucent ones, matching /// legacy GT ore blocks. @SideOnly(Side.CLIENT) +@ThreadSafeISBRH(perThread = false) public final class ShapeBlockRenderingHandler implements ISimpleBlockRenderingHandler { private static final int RENDER_ID = RenderingRegistry.getNextAvailableRenderId(); @@ -43,44 +48,35 @@ public void renderInventoryBlock(Block block, int metadata, int modelId, RenderB if (!(block instanceof ShapeBlock shape)) return; block.setBlockBoundsForItemRender(); renderer.setRenderBoundsFromBlock(block); - renderer.useInventoryTint = true; GL11.glRotatef(90.0F, 0.0F, 1.0F, 0.0F); GL11.glTranslatef(-0.5F, -0.5F, -0.5F); Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); - drawInventoryLayer(renderer, tessellator, shape, metadata, 0); - drawInventoryLayer(renderer, tessellator, shape, metadata, 1); + drawInventoryLayer(renderer, tessellator, shape, shape.baseIcon(), 0xFFFFFF); + drawInventoryLayer(renderer, tessellator, shape, shape.materialIcon(metadata), shape.tintFor(metadata)); tessellator.draw(); GL11.glTranslatef(0.5F, 0.5F, 0.5F); } private static void drawInventoryLayer(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape, - int metadata, int layer) { - shape.setLayerOverride(layer); - try { - int color = shape.getRenderColor(metadata); - tessellator.setColorOpaque_F( - (color >> 16 & 255) / 255.0F, - (color >> 8 & 255) / 255.0F, - (color & 255) / 255.0F); - drawInventoryFace(renderer, tessellator, shape, 0, metadata, 0.0F, -1.0F, 0.0F); - drawInventoryFace(renderer, tessellator, shape, 1, metadata, 0.0F, 1.0F, 0.0F); - drawInventoryFace(renderer, tessellator, shape, 2, metadata, 0.0F, 0.0F, -1.0F); - drawInventoryFace(renderer, tessellator, shape, 3, metadata, 0.0F, 0.0F, 1.0F); - drawInventoryFace(renderer, tessellator, shape, 4, metadata, -1.0F, 0.0F, 0.0F); - drawInventoryFace(renderer, tessellator, shape, 5, metadata, 1.0F, 0.0F, 0.0F); - } - finally { - shape.setLayerOverride(-1); - } + IIcon icon, int color) { + tessellator.setColorOpaque_F( + (color >> 16 & 255) / 255.0F, + (color >> 8 & 255) / 255.0F, + (color & 255) / 255.0F); + drawInventoryFace(renderer, tessellator, shape, 0, icon, 0.0F, -1.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 1, icon, 0.0F, 1.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 2, icon, 0.0F, 0.0F, -1.0F); + drawInventoryFace(renderer, tessellator, shape, 3, icon, 0.0F, 0.0F, 1.0F); + drawInventoryFace(renderer, tessellator, shape, 4, icon, -1.0F, 0.0F, 0.0F); + drawInventoryFace(renderer, tessellator, shape, 5, icon, 1.0F, 0.0F, 0.0F); } private static void drawInventoryFace(RenderBlocks renderer, Tessellator tessellator, ShapeBlock shape, int side, - int metadata, float nx, float ny, float nz) { - IIcon icon = shape.getIcon(side, metadata); + IIcon icon, float nx, float ny, float nz) { tessellator.setNormal(nx, ny, nz); switch (side) { case 0 -> renderer.renderFaceYNeg(shape, 0.0D, 0.0D, 0.0D, icon); @@ -89,7 +85,6 @@ private static void drawInventoryFace(RenderBlocks renderer, Tessellator tessell case 3 -> renderer.renderFaceZPos(shape, 0.0D, 0.0D, 0.0D, icon); case 4 -> renderer.renderFaceXNeg(shape, 0.0D, 0.0D, 0.0D, icon); case 5 -> renderer.renderFaceXPos(shape, 0.0D, 0.0D, 0.0D, icon); - default -> throw new IllegalArgumentException("side must be 0..5, got " + side); } } @@ -97,18 +92,42 @@ private static void drawInventoryFace(RenderBlocks renderer, Tessellator tessell public boolean renderWorldBlock(IBlockAccess world, int x, int y, int z, Block block, int modelId, RenderBlocks renderer) { if (!(block instanceof ShapeBlock shape)) return false; - boolean renderedBase = drawWorldLayer(renderer, shape, x, y, z, 0); - boolean renderedOverlay = drawWorldLayer(renderer, shape, x, y, z, 1); - return renderedBase || renderedOverlay; + // The destroy-progress crack arrives through RenderBlocks.renderBlockUsingTexture, which sets its own + // override texture; a single draw stamps that texture once. + if (renderer.hasOverrideBlockTexture()) return renderer.renderStandardBlock(shape, x, y, z); + int meta = world.getBlockMetadata(x, y, z); + boolean rendered = drawWorldLayer(renderer, shape, x, y, z, shape.baseIcon(), 0xFFFFFF); + drawWorldLayer(renderer, shape, x, y, z, shape.materialIcon(meta), shape.tintFor(meta)); + return rendered; } - private static boolean drawWorldLayer(RenderBlocks renderer, ShapeBlock shape, int x, int y, int z, int layer) { - shape.setLayerOverride(layer); + /// Draws one layer as a standard block of `icon` tinted `color`, mirroring [RenderBlocks#renderStandardBlock]'s + /// dispatch. That method is not called directly: it takes its color from [ShapeBlock#colorMultiplier], which is + /// white for every composite, in place of the per-layer color. + private static boolean drawWorldLayer(RenderBlocks renderer, ShapeBlock shape, int x, int y, int z, IIcon icon, + int color) { + renderer.setOverrideBlockTexture(icon); try { - return renderer.renderStandardBlock(shape, x, y, z); + float red = (color >> 16 & 255) / 255.0F; + float green = (color >> 8 & 255) / 255.0F; + float blue = (color & 255) / 255.0F; + if (EntityRenderer.anaglyphEnable) { + float anaglyphRed = (red * 30.0F + green * 59.0F + blue * 11.0F) / 100.0F; + float anaglyphGreen = (red * 30.0F + green * 70.0F) / 100.0F; + float anaglyphBlue = (red * 30.0F + blue * 70.0F) / 100.0F; + red = anaglyphRed; + green = anaglyphGreen; + blue = anaglyphBlue; + } + if (Minecraft.isAmbientOcclusionEnabled() && shape.getLightValue() == 0) { + return renderer.partialRenderBounds ? + renderer.renderStandardBlockWithAmbientOcclusionPartial(shape, x, y, z, red, green, blue) : + renderer.renderStandardBlockWithAmbientOcclusion(shape, x, y, z, red, green, blue); + } + return renderer.renderStandardBlockWithColorMultiplier(shape, x, y, z, red, green, blue); } finally { - shape.setLayerOverride(-1); + renderer.clearOverrideBlockTexture(); } } } diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index e215c5d2..cdf951ad 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -33,14 +33,8 @@ void hasBaseTextureReflectsWhetherAVariantBaseWasDeclared() { @Test void layerZeroIsUntintedWhenABaseTextureExists() { - withBaseTexture.setLayerOverride(0); - try { - assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(0)); - assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(999)); - } - finally { - withBaseTexture.setLayerOverride(-1); - } + assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(0)); + assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(999)); } @Test From e0392f0a1437050372d3b2bc988af2c8f5e57cb1 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:26:06 -0500 Subject: [PATCH 17/18] Fix tint docs and trim composite rendering tests --- .../materiallib/api/MaterialBuilder.java | 2 +- .../api/ShapeFluidInContainer.java | 14 ++++---- .../materiallib/api/StandardProperties.java | 4 +-- .../api/ResourceUtilBaseTexturePathTest.java | 35 ------------------- .../materiallib/api/ShapeBlockTest.java | 32 +++-------------- .../examples/ExampleContentTest.java | 17 ++++----- 6 files changed, 21 insertions(+), 83 deletions(-) delete mode 100644 src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java diff --git a/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java b/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java index 8d5d4e32..06d9316c 100644 --- a/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java +++ b/src/main/java/com/ruling_0/materiallib/api/MaterialBuilder.java @@ -37,7 +37,7 @@ public MaterialBuilder setTint(int tint) { } /// Sets [StandardProperties#FLUID_TINT], the ARGB tint applied to a fluid shape's fill icon in place of - /// [#setTint]. + /// [StandardProperties#TINT]. public MaterialBuilder setFluidTint(int fluidTint) { return setProperty(StandardProperties.FLUID_TINT, fluidTint); } diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java index a779ef85..d34f483a 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java @@ -22,11 +22,11 @@ /// the registry enforces that at resolve. /// /// Renders in two passes: an untinted empty-container texture underneath the texture set's texture for this -/// shape, which supplies the fluid fill and is tinted with [StandardProperties#TINT]. The container looks the -/// same for every material, so the empty texture is a property of the shape rather than of a texture set: it -/// defaults to `:materials/_empty` in the shape's own domain, or the path -/// [FluidInContainerShapeBuilder#emptyIcon] sets. For a path naming no existing texture file, see -/// [#registerIcons]. +/// shape, which supplies the fluid fill and is tinted with [StandardProperties#CELL_TINT] when the material sets +/// it, or with the fluid's fill tint (see [ShapeFluid#tintOf]) otherwise. The container looks the same for every +/// material, so the empty texture is a property of the shape rather than of a texture set: it defaults to +/// `:materials/_empty` in the shape's own domain, or the path [FluidInContainerShapeBuilder#emptyIcon] +/// sets. For a path naming no existing texture file, see [#registerIcons]. public class ShapeFluidInContainer extends ShapeItem { private final Shape fluidShape; @@ -143,9 +143,7 @@ public IIcon getIconFromDamage(int damage) { return emptyIcon; } - /// White for the untinted container base in pass 0, and the fill tint for every later pass: - /// [StandardProperties#CELL_TINT] when the material sets it, or the fluid fill tint (see [ShapeFluid#tintOf]) - /// otherwise. + /// White for the untinted container base in pass 0, the fill tint (see the class doc) for every later pass. @Override @SideOnly(Side.CLIENT) public int getColorFromItemStack(ItemStack stack, int renderPass) { diff --git a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java index bf7f1a02..e4e9ecd9 100644 --- a/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java +++ b/src/main/java/com/ruling_0/materiallib/api/StandardProperties.java @@ -27,8 +27,8 @@ private StandardProperties() {} public static final Property TINT = Property.of(MaterialLib.MODID, "tint", 0xFFFFFFFF); /// ARGB tint applied to a fluid shape's fill icon in place of [#TINT], for fluid art that already encodes its - /// color. Null when unset, falling back to [#TINT]. Applies only to the fluid fill layer; every other shape a - /// material generates uses [#TINT]. + /// color. Null when unset, falling back to [#TINT]. Applies to a fluid's fill layer and to the fill layer of a + /// [ShapeFluidInContainer] holding it; every other shape uses [#TINT]. public static final Property FLUID_TINT = Property.of(MaterialLib.MODID, "fluidTint"); /// ARGB tint applied to a [ShapeBlock#hasBaseTexture] composite's overlay icon layer in place of [#TINT], for diff --git a/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java b/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java deleted file mode 100644 index f8a11236..00000000 --- a/src/test/java/com/ruling_0/materiallib/api/ResourceUtilBaseTexturePathTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.ruling_0.materiallib.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import net.minecraft.util.ResourceLocation; - -import com.gtnewhorizon.gtnhlib.util.ResourceUtil; - -import org.junit.jupiter.api.Test; - -/// Pins the block-texture path construction [ShapeBlock]'s base-texture existence check and registration both -/// resolve through. 1.7.10 resource lookups are case-sensitive, so a mixed-case domain must survive intact. -class ResourceUtilBaseTexturePathTest { - - @Test - void vanillaDomainedPathResolvesUnderTheBlockAtlasConvention() { - ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("minecraft:stone"); - assertEquals("minecraft", location.getResourceDomain()); - assertEquals("textures/blocks/stone.png", location.getResourcePath()); - } - - @Test - void mixedCaseDomainIsPreservedRatherThanLowercased() { - ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("GalacticraftCore:moon"); - assertEquals("GalacticraftCore", location.getResourceDomain()); - assertEquals("textures/blocks/moon.png", location.getResourcePath()); - } - - @Test - void domainlessPathDefaultsToMinecraft() { - ResourceLocation location = ResourceUtil.getCompleteBlockTextureResourceLocation("stone"); - assertEquals("minecraft", location.getResourceDomain()); - assertEquals("textures/blocks/stone.png", location.getResourcePath()); - } -} diff --git a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java index cdf951ad..78b90eaf 100644 --- a/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java +++ b/src/test/java/com/ruling_0/materiallib/api/ShapeBlockTest.java @@ -1,15 +1,12 @@ package com.ruling_0.materiallib.api; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; -/// Headless coverage for [ShapeBlock]'s base-texture and render-type bookkeeping. [ShapeBlock#getRenderColor]'s -/// tinted branch resolves a material through the process-wide [MaterialRegistry#instance], which throws on lookup -/// before MaterialLib's preInit, so only the untinted (layer 0) branch is exercised; the compositing itself needs -/// a live client. +/// Headless coverage for [ShapeBlock]'s block-level color callbacks on a composite. The tinted branch a plain +/// block takes resolves a material through the process-wide [MaterialRegistry#instance], which throws on lookup +/// before MaterialLib's preInit; the compositing itself needs a live client. class ShapeBlockTest { private final ShapeBlock withBaseTexture = new ShapeBlock( @@ -23,29 +20,10 @@ class ShapeBlockTest { BlockBehavior.NONE, null); - private final ShapeBlock withoutBaseTexture = new ShapeBlock("testmod", "ingot", "%s Ingot", "ingot"); - - @Test - void hasBaseTextureReflectsWhetherAVariantBaseWasDeclared() { - assertTrue(withBaseTexture.hasBaseTexture()); - assertFalse(withoutBaseTexture.hasBaseTexture()); - } - @Test - void layerZeroIsUntintedWhenABaseTextureExists() { + void aCompositeReportsWhiteToBlockLevelColorCallbacks() { assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(0)); + assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(7)); assertEquals(0xFFFFFF, withBaseTexture.getRenderColor(999)); } - - @Test - void getRenderTypeReflectsWhateverWasSet() { - assertEquals(0, withBaseTexture.getRenderType()); - withBaseTexture.setRenderType(64); - try { - assertEquals(64, withBaseTexture.getRenderType()); - } - finally { - withBaseTexture.setRenderType(0); - } - } } diff --git a/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java index 85ce9f90..4f57078c 100644 --- a/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java +++ b/src/test/java/com/ruling_0/materiallib/examples/ExampleContentTest.java @@ -13,18 +13,15 @@ class ExampleContentTest { @Test - void testOreStoneBaseTextureResolvesToTheVanillaStoneTexture() { - ResourceLocation location = ResourceUtil + void variantBaseTexturesResolveToTheVanillaBlockTextures() { + ResourceLocation stone = ResourceUtil .getCompleteBlockTextureResourceLocation(ExampleContent.TEST_ORE_STONE_BASE_TEXTURE); - assertEquals("minecraft", location.getResourceDomain()); - assertEquals("textures/blocks/stone.png", location.getResourcePath()); - } + assertEquals("minecraft", stone.getResourceDomain()); + assertEquals("textures/blocks/stone.png", stone.getResourcePath()); - @Test - void testOreCobblestoneBaseTextureResolvesToTheVanillaCobblestoneTexture() { - ResourceLocation location = ResourceUtil + ResourceLocation cobblestone = ResourceUtil .getCompleteBlockTextureResourceLocation(ExampleContent.TEST_ORE_COBBLESTONE_BASE_TEXTURE); - assertEquals("minecraft", location.getResourceDomain()); - assertEquals("textures/blocks/cobblestone.png", location.getResourcePath()); + assertEquals("minecraft", cobblestone.getResourceDomain()); + assertEquals("textures/blocks/cobblestone.png", cobblestone.getResourcePath()); } } From 06c0e74394cc21d8af99a9a45aad4db7ba38fdb0 Mon Sep 17 00:00:00 2001 From: Ruling-0 <47913168+Ruling-0@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:50:36 -0500 Subject: [PATCH 18/18] Trim log occurence mentions --- src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java | 4 ++-- src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java | 4 ++-- .../com/ruling_0/materiallib/api/ShapeFluidInContainer.java | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java index 847a8b4f..205ab71b 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeBlock.java @@ -171,8 +171,8 @@ public void registerBlockIcons(IIconRegister register) { } } - /// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder -- logged - /// once -- if it does not. + /// Registers [#baseTexture] if it names an existing file, or the [ShapeIcons#EMPTY_ICON] placeholder if it + /// does not. private IIcon registerBaseIcon(IIconRegister register) { if (ResourceUtil.resourceExists(ResourceUtil.getCompleteBlockTextureResourceLocation(baseTexture))) { return register.registerIcon(baseTexture); diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java index 5786ed64..067b914d 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluid.java @@ -176,8 +176,8 @@ FluidStack fluidStack(Material material, int amount) { } /// Binds each material's still and flowing fluid icon from [#iconPath], or the [ShapeIcons#EMPTY_ICON] - /// placeholder -- logged once per material -- if that path names no existing texture file. Fluid textures live - /// on the block atlas, so this runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). + /// placeholder if that path names no existing texture file. Fluid textures live on the block atlas, so this + /// runs from a blocks texture-stitch on the client (see [ShapeFluidIcons]). @SideOnly(Side.CLIENT) void registerIcons(IIconRegister register) { for (Material material : served.get()) { diff --git a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java index d34f483a..febb5e04 100644 --- a/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java +++ b/src/main/java/com/ruling_0/materiallib/api/ShapeFluidInContainer.java @@ -102,7 +102,7 @@ void registerContainers(ShapeFluid fluid) { } /// Registers this container's fill icons, then its base icon at [#emptyIconPath], or the - /// [ShapeIcons#EMPTY_ICON] placeholder -- logged once -- if that path names no existing texture file. + /// [ShapeIcons#EMPTY_ICON] placeholder if that path names no existing texture file. @Override @SideOnly(Side.CLIENT) public void registerIcons(IIconRegister register) {