Allow non-block textures in JSON models (#438)

Co-authored-by: Martin Robertz <dream-master@gmx.net>
Co-authored-by: RecursivePineapple <recursive_pineapple@proton.me>
This commit is contained in:
authored and GitHub committed 2026-09-04 19:10:27 +00:00
1 parent 4abbd38535
commit d62a9db8b7
23 files changed
+223 -84

No files matched your search

+7 -5
View File
@@ -3,10 +3,13 @@
In general, it is designed to mimic the latest version.
### Intentional deviations
Texture references are assumed to be in `assets/<domain>/blocks` by
default, to match 7.10 conventions. However, textures specified by
`domain:block/whatever` are remapped to `domain:whatever`, to support
importing models from modern.
None (for now).
### Former deviations
Texture references are no longer assumed to be the same as 7.10 locations.
Instead, they are resolved from `assets/namespace/textures` like modern
does. This also means you can now use *any* texture in your ISBRH, and
GTNHLib will inject it into the block atlas. Please use this power responsibly.
Resource packs can now replace most blocks, similarly to modern.
Note that this primarily works on blocks with ISBRHS - if the block
@@ -164,7 +167,6 @@ The loader will:
Only implement the traits you need.
### TODO
- Autoload item textures too.
- Add smooth shading to ModelISBRH.
- Add face culling to models.
- Implement UV locking.
@@ -35,6 +35,7 @@ import com.gtnewhorizon.gtnhlib.teams.TeamAdminCommand;
import com.gtnewhorizon.gtnhlib.teams.TeamCommand;
import com.gtnewhorizon.gtnhlib.test.block.BlockRngTest;
import com.gtnewhorizon.gtnhlib.test.block.BlockTest;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestAo;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestLectern;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestTint;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestTintMul;
@@ -70,6 +71,7 @@ public class CommonProxy {
if (GTNHLibConfig.enableTestBlocks) {
BlockTest.register();
BlockTestAo.register();
BlockTestLectern.register();
BlockTestTint.register();
BlockTestTintMul.register();
@@ -7,7 +7,6 @@ import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import net.minecraftforge.common.util.ForgeDirection;
@@ -106,8 +105,6 @@ public class ModelDeserializer implements JsonDeserializer<JSONModel> {
return ret;
}
private static Pattern TEXEX = Pattern.compile("^([^:]+:)blocks?/");
private static Object2ObjectOpenHashMap<String, String> loadTextures(JsonObject in) {
final var textures = new Object2ObjectOpenHashMap<String, String>();
@@ -116,15 +113,6 @@ public class ModelDeserializer implements JsonDeserializer<JSONModel> {
for (Map.Entry<String, JsonElement> e : in.getAsJsonObject("textures").entrySet()) {
String s = e.getValue().getAsString();
// If it's a texture variable, no munging is needed
if (!s.startsWith("#")) {
// Add the default domain, if it's absent.
if (!s.contains(":")) s = "minecraft:" + s;
// Strip "block/" if present
s = TEXEX.matcher(s).replaceFirst("$1");
}
// The key is always a variable, so prepend accordingly
final var key = e.getKey();
textures.put(key.startsWith("#") ? key : "#" + key, s);
@@ -4,8 +4,6 @@ import static com.gtnewhorizon.gtnhlib.GTNHLibConfig.modelCacheSize;
import static com.gtnewhorizon.gtnhlib.client.model.unbaked.MissingModel.MISSING_MODEL;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import java.util.List;
import net.minecraft.block.Block;
import net.minecraft.client.resources.FallbackResourceManager;
import net.minecraft.client.resources.IResourceManager;
@@ -38,8 +36,6 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.registry.GameData;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectLists;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
/// Handles model loading and caching. All caches are size-based - this means that if a model has enough parents, it may
@@ -199,7 +195,7 @@ public class ModelRegistry {
// Gather the list of modeled blocks and their textures
final var modeledBlocks = new ObjectOpenHashSet<String>();
final var texturesToLoad = new ObjectArrayList<String>();
final var texturesToLoad = new ObjectOpenHashSet<String>();
for (var pack : resourcePacks) {
if (!(pack instanceof ModelResourcePack mrp)) continue;
@@ -226,6 +222,8 @@ public class ModelRegistry {
texturesToLoad.addAll(info.textureNames());
}
// Sadly, this field is untyped and we can't do anything about it... not easily, anyway.
// noinspection unchecked
GameData.getBlockRegistry().registryObjects.forEach((s, b) -> {
if (!(s instanceof String name)) return;
if (!(b instanceof Block block)) return;
@@ -258,15 +256,15 @@ public class ModelRegistry {
public static class EventHandler {
private static @NotNull List<String> texturesToLoad = ObjectLists.emptyList();
private static @NotNull ObjectOpenHashSet<String> texturesToLoad = ObjectOpenHashSet.of();
@SubscribeEvent
@SideOnly(Side.CLIENT)
public void onTextureStitch(TextureStitchEvent.Pre event) {
for (var texture : texturesToLoad) {
event.map.registerIcon(texture.replaceFirst("^minecraft:", ""));
TexHelper.registerTexture(event.map, texture);
}
texturesToLoad = ObjectLists.emptyList(); // don't need it anymore
texturesToLoad = ObjectOpenHashSet.of(); // don't need it anymore
}
}
}
@@ -0,0 +1,30 @@
package com.gtnewhorizon.gtnhlib.client.model.loading;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.IIcon;
/// This class exist to inject textures into one or more atlases, even if they were never intended to be there.
public class TexHelper {
static IIcon registerTexture(TextureMap atlas, String texture) {
var icon = atlas.registerIcon(texture);
if (!(icon instanceof BaseCheckedTex bCT)) {
// Something has gone terribly wrong, and the map isn't returning a normal sprite
MODEL_LOGGER.warn("Failed to register {} in foreign atlas {}", texture, atlas);
return icon;
}
bCT.nhlib$setHasBase(false);
return icon;
}
/// Injected into [TextureAtlasSprite] so we can check if it has an implied base like `items` or `blocks`.`
public interface BaseCheckedTex {
void nhlib$setHasBase(boolean hasBase);
boolean nhlib$hasBase();
}
}
@@ -254,7 +254,6 @@ public class JSONModel implements UnbakedModel {
// TODO Doesn't account for UV rotation settings atm
protected void bakeSprite(ModelQuadViewMutable quad, String name) {
name = name.replaceFirst("^minecraft:", "");
final var icon = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite(name);
final float minU = icon.getMinU();
final float minV = icon.getMinV();
@@ -28,6 +28,8 @@ public enum Mixins implements IMixins {
"models.MixinFileResourcePack",
"models.MixinFolderResourcePack",
"models.MixinRenderBlocks",
"models.MixinTextureAtlasSprite",
"models.MixinTextureMap",
"models.SRRMAccessor").setPhase(Phase.EARLY).setApplyIf(() -> true)),
MODEL_PARTICLE_FIXES(new MixinBuilder("Override particle icons for models.").addClientMixins(
"models.particles.MixinEffectRenderer",
@@ -0,0 +1,25 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import com.gtnewhorizon.gtnhlib.client.model.loading.TexHelper;
@Mixin(TextureAtlasSprite.class)
public class MixinTextureAtlasSprite implements TexHelper.BaseCheckedTex {
@Unique
private boolean nhlib$hasBase = true;
@Override
public void nhlib$setHasBase(boolean hasBase) {
nhlib$hasBase = hasBase;
}
@Override
public boolean nhlib$hasBase() {
return nhlib$hasBase;
}
}
@@ -0,0 +1,45 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.texture.TextureMap;
import net.minecraft.util.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import com.gtnewhorizon.gtnhlib.client.model.loading.TexHelper;
import com.llamalad7.mixinextras.sugar.Local;
@SuppressWarnings("UnusedMixin")
@Mixin(TextureMap.class)
public abstract class MixinTextureMap {
@Shadow
protected abstract ResourceLocation completeResourceLocation(ResourceLocation location, int mipId);
@Redirect(
method = "loadTextureAtlas",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/texture/TextureMap;completeResourceLocation(Lnet/minecraft/util/ResourceLocation;I)Lnet/minecraft/util/ResourceLocation;",
ordinal = 0))
private ResourceLocation nhlib$unbaseTextures(TextureMap instance, ResourceLocation location, int mipId,
@Local(name = "textureatlassprite") TextureAtlasSprite sprite) {
if (((TexHelper.BaseCheckedTex) sprite).nhlib$hasBase()) return completeResourceLocation(location, mipId);
// I don't *like* completely replacing the internals, but it's better than an overwrite.
// And more importantly, I'm really not sure how I'd do this (cleanly) with a different method.
if (mipId == 0) {
return new ResourceLocation(
location.getResourceDomain(),
String.format("textures/%s%s", location.getResourcePath(), ".png"));
}
var path = location.getResourcePath().split("/", 2);
return new ResourceLocation(
location.getResourceDomain(),
String.format("textures/%s/mipmaps/%s.%d%s", path[0], path[1], mipId, ".png"));
}
}
@@ -2,24 +2,12 @@ package com.gtnewhorizon.gtnhlib.test.block;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty.AbstractDirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.util.DirectionUtil;
import cpw.mods.fml.common.registry.GameRegistry;
/// A plain cube, nothing special about it.
public class BlockTest extends Block {
/// This default blockstate
public final AbstractDirectionBlockProperty FACING_PROP = (AbstractDirectionBlockProperty) DirectionBlockProperty
.facing();
public BlockTest() {
super(Material.wood);
}
@@ -28,25 +16,11 @@ public class BlockTest extends Block {
final var testBlock = new BlockTest();
testBlock.setBlockName("model_test");
GameRegistry.registerBlock(testBlock, "model_test");
/// This method allows you to adapt any property to itemstacks, with some default value. This is mostly relevant
/// for modeled blocks, as they may require some state to match *any* of their models. If you're defining a
/// custom blockstate, it's better to add an ItemStack default yourself - see [BlockTestTintMul] for an example
/// of that.
BlockPropertyRegistry.registerBlockItemProperty(testBlock, testBlock.FACING_PROP, ForgeDirection.EAST);
}
/// Okay so it's transparent, but that's it. That's the only special thing about it.
@Override
public boolean isOpaqueCube() {
return false;
}
@Override
public void onBlockPlacedBy(World worldIn, int x, int y, int z, EntityLivingBase placer, ItemStack itemIn) {
var dir = DirectionUtil.yawToDirection(placer.rotationYaw);
/// This is a shortcut that works because this block only has a single meta property. See [BlockTestTintMul] for
/// an example with non-meta blockstate.
worldIn.setBlockMetadataWithNotify(x, y, z, FACING_PROP.getMeta(dir, 0), 2);
}
}
@@ -0,0 +1,52 @@
package com.gtnewhorizon.gtnhlib.test.block;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty.AbstractDirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.util.DirectionUtil;
import cpw.mods.fml.common.registry.GameRegistry;
public class BlockTestAo extends Block {
/// This default blockstate
public final AbstractDirectionBlockProperty FACING_PROP = (AbstractDirectionBlockProperty) DirectionBlockProperty
.facing();
public BlockTestAo() {
super(Material.wood);
}
public static void register() {
final var testBlock = new BlockTestAo();
testBlock.setBlockName("model_test_ao");
GameRegistry.registerBlock(testBlock, "model_test_ao");
/// This method allows you to adapt any property to itemstacks, with some default value. This is mostly relevant
/// for modeled blocks, as they may require some state to match *any* of their models. If you're defining a
/// custom blockstate, it's better to add an ItemStack default yourself - see [BlockTestTintMul] for an example
/// of that.
BlockPropertyRegistry.registerBlockItemProperty(testBlock, testBlock.FACING_PROP, ForgeDirection.EAST);
}
@Override
public boolean isOpaqueCube() {
return false;
}
@Override
public void onBlockPlacedBy(World worldIn, int x, int y, int z, EntityLivingBase placer, ItemStack itemIn) {
var dir = DirectionUtil.yawToDirection(placer.rotationYaw);
/// This is a shortcut that works because this block only has a single meta property. See [BlockTestTintMul] for
/// an example with non-meta blockstate.
worldIn.setBlockMetadataWithNotify(x, y, z, FACING_PROP.getMeta(dir, 0), 2);
}
}
@@ -6,7 +6,7 @@ import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import cpw.mods.fml.common.registry.GameRegistry;
public class BlockTestLectern extends BlockTest {
public class BlockTestLectern extends BlockTestAo {
public BlockTestLectern() {
super();
@@ -1,8 +1,5 @@
{
"variants": {
"facing=north": { "model": "gtnhlib:blocks/old_stairs", "y": 270, "uvlock": true },
"facing=south": { "model": "gtnhlib:blocks/old_stairs", "y": 90, "uvlock": true },
"facing=west": { "model": "gtnhlib:blocks/old_stairs", "y": 0, "uvlock": true },
"facing=east": { "model": "gtnhlib:blocks/old_stairs", "y": 180, "uvlock": true }
"": { "model": "gtnhlib:blocks/model_test" }
}
}
@@ -0,0 +1,8 @@
{
"variants": {
"facing=north": { "model": "gtnhlib:blocks/old_stairs", "y": 270, "uvlock": true },
"facing=south": { "model": "gtnhlib:blocks/old_stairs", "y": 90, "uvlock": true },
"facing=west": { "model": "gtnhlib:blocks/old_stairs", "y": 0, "uvlock": true },
"facing=east": { "model": "gtnhlib:blocks/old_stairs", "y": 180, "uvlock": true }
}
}
@@ -1,8 +1,19 @@
{
"variants": {
"facing=north": { "model": "minecraft:block/lectern", "y": 270, "uvlock": true },
"facing=south": { "model": "minecraft:block/lectern", "y": 90, "uvlock": true },
"facing=west": { "model": "minecraft:block/lectern", "y": 0, "uvlock": true },
"facing=east": { "model": "minecraft:block/lectern", "y": 180, "uvlock": true }
"facing=east": {
"model": "minecraft:block/lectern",
"y": 90
},
"facing=north": {
"model": "minecraft:block/lectern"
},
"facing=south": {
"model": "minecraft:block/lectern",
"y": 180
},
"facing=west": {
"model": "minecraft:block/lectern",
"y": 270
}
}
}
@@ -1,6 +1,12 @@
{
"parent": "block/cube_all",
"parent": "block/cube",
"textures": {
"all": "minecraft:stone"
"particle": "gtnhlib:blocks/test",
"north": "blocks/stone",
"south": "minecraft:blocks/oak_planks",
"east": "items/blaze_rod",
"west": "minecraft:items/apple",
"up": "gtnhlib:blocks/test",
"down": "gtnhlib:blocks/test"
}
}
@@ -1,10 +1,10 @@
{
"parent": "block/cube",
"textures": {
"particle": "gtnhlib:block/test",
"north": "gtnhlib:block/test",
"south": "gtnhlib:block/test",
"east": "gtnhlib:block/test",
"particle": "gtnhlib:blocks/test",
"north": "gtnhlib:blocks/test",
"south": "gtnhlib:blocks/test",
"east": "gtnhlib:blocks/test",
"west": "gtnhlib:blocks/test",
"up": "gtnhlib:blocks/test",
"down": "gtnhlib:blocks/test"
@@ -3,8 +3,8 @@
"credit": "Made with Blockbench",
"parent": "block/cube",
"textures": {
"particle": "planks_oak",
"down": "planks_oak"
"particle": "blocks/planks_oak",
"down": "blocks/planks_oak"
},
"elements": [
{
@@ -56,4 +56,4 @@
"scale": [0.5, 1.2, 1]
}
}
}
}
@@ -1,8 +1,8 @@
{
"parent": "minecraft:block/stairs",
"textures": {
"bottom": "minecraft:planks_oak",
"side": "minecraft:planks_oak",
"top": "minecraft:planks_oak"
"bottom": "minecraft:blocks/planks_oak",
"side": "minecraft:blocks/planks_oak",
"top": "minecraft:blocks/planks_oak"
}
}
@@ -1,6 +1,6 @@
{
"parent": "block/cube_all",
"textures": {
"all": "gtnhlib:test_one"
"all": "gtnhlib:blocks/test_one"
}
}
@@ -1,6 +1,6 @@
{
"parent": "block/cube_all",
"textures": {
"all": "gtnhlib:test_three"
"all": "gtnhlib:blocks/test_three"
}
}
@@ -1,6 +1,6 @@
{
"parent": "block/cube_all",
"textures": {
"all": "gtnhlib:test_two"
"all": "gtnhlib:blocks/test_two"
}
}
@@ -13,12 +13,12 @@
}
},
"textures": {
"particle": "minecraft:lectern_sides",
"bottom": "minecraft:oak_planks",
"base": "minecraft:lectern_base",
"front": "minecraft:lectern_front",
"sides": "minecraft:lectern_sides",
"top": "minecraft:lectern_top"
"particle": "minecraft:blocks/lectern_sides",
"bottom": "minecraft:blocks/oak_planks",
"base": "minecraft:blocks/lectern_base",
"front": "minecraft:blocks/lectern_front",
"sides": "minecraft:blocks/lectern_sides",
"top": "minecraft:blocks/lectern_top"
},
"elements": [
{