Add proper automatic model detection (#313)

Co-authored-by: Ruling-0 <47913168+Ruling-0@users.noreply.github.com>
This commit is contained in:
ah-OOG-ahandRuling-0 authored and GitHub committed 2026-03-16 09:47:13 -04:00
1 parent 43db33a076
commit ae2de77b74
33 files changed
+641 -376

No files matched your search

+1
View File
@@ -5,6 +5,7 @@ plugins {
minecraft {
//extraRunJvmArguments.add("-Dgtnhlib.dumpClass=true")
//extraRunJvmArguments.addAll("-Dlegacy.debugClassLoading=true", "-Dlegacy.debugClassLoadingFiner=true", "-Dlegacy.debugClassLoadingSave=true")
}
// Add a Java 17 sourceset for including code optimized for newer Java versions
@@ -2,15 +2,12 @@ package com.gtnewhorizon.gtnhlib;
import static com.gtnewhorizon.gtnhlib.GTNHLib.MODID;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.SimpleReloadableResourceManager;
import net.minecraft.item.Item;
import net.minecraft.util.ChatComponentText;
import net.minecraft.util.EnumChatFormatting;
import net.minecraft.util.IChatComponent;
import net.minecraftforge.client.ClientCommandHandler;
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.common.MinecraftForge;
import com.gtnewhorizon.gtnhlib.client.ResourcePackUpdater.ResourcePackUpdateEventHandler;
@@ -92,19 +89,6 @@ public class ClientProxy extends CommonProxy {
final var resourceManager = ((SimpleReloadableResourceManager) Minecraft.getMinecraft().getResourceManager());
resourceManager.registerReloadListener(new ModelRegistry.ReloadListener());
MinecraftForge.EVENT_BUS.register(new ModelRegistry.EventHandler());
// Applied Item Render for Block Use ModelISBRH
for (Object obj : Block.blockRegistry) {
try {
Block block = (Block) obj;
if (block.getRenderType() == ModelISBRH.JSON_ISBRH_ID) {
Item item = Item.getItemFromBlock(block);
if (item != null) {
MinecraftForgeClient.registerItemRenderer(item, ModelISBRH.INSTANCE);
}
}
} catch (Exception ignored) {} // Some blocks may throw an Exception here, we can simply ignore it.
}
}
@Override
@@ -2,24 +2,14 @@ package com.gtnewhorizon.gtnhlib;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.isObf;
import net.minecraft.block.Block;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.server.MinecraftServer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.IChatComponent;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.common.util.ForgeDirection;
import com.gtnewhorizon.gtnhlib.blockstate.command.BlockStateCommand;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockPropertyTrait;
import com.gtnewhorizon.gtnhlib.blockstate.init.BlockPropertyInit;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.brigadier.BrigadierApi;
import com.gtnewhorizon.gtnhlib.chat.ChatComponentCustomRegistry;
import com.gtnewhorizon.gtnhlib.chat.customcomponents.ChatComponentEnergy;
@@ -39,7 +29,6 @@ import com.gtnewhorizon.gtnhlib.network.PacketViewDistance;
import com.gtnewhorizon.gtnhlib.test.block.BlockTest;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestTint;
import com.gtnewhorizon.gtnhlib.test.block.BlockTestTintMul;
import com.gtnewhorizon.gtnhlib.test.block.TileTestTintMul;
import com.gtnewhorizon.gtnhlib.test.item.TestItem;
import com.gtnewhorizon.gtnhlib.util.numberformatting.NumberFormatConfig;
import com.gtnewhorizon.gtnhlib.util.numberformatting.NumberFormatUtil;
@@ -69,52 +58,9 @@ public class CommonProxy {
GTNHLib.info("GTNHLib version " + Tags.VERSION + " loaded.");
if (GTNHLibConfig.enableTestBlocks) {
GameRegistry.registerBlock(new BlockTest(), "model_test");
GameRegistry.registerBlock(new BlockTestTint(), "model_test_tint");
Block block = new BlockTestTintMul();
GameRegistry.registerBlock(block, "model_test_tint_mul");
GameRegistry.registerTileEntity(TileTestTintMul.class, "tile_test_tint_mul");
DirectionBlockProperty property = new DirectionBlockProperty() {
@Override
public String getName() {
return "facing";
}
@Override
public boolean hasTrait(BlockPropertyTrait trait) {
return switch (trait) {
case SupportsWorld, WorldMutable, StackMutable, SupportsStacks -> true;
default -> false;
};
}
@Override
public ForgeDirection getValue(IBlockAccess world, int x, int y, int z) {
TileEntity te = world.getTileEntity(x, y, z);
if (te instanceof TileTestTintMul tile) {
return tile.getFacing();
}
return ForgeDirection.NORTH;
}
@Override
public void setValue(World world, int x, int y, int z, ForgeDirection value) {
TileEntity te = world.getTileEntity(x, y, z);
if (te instanceof TileTestTintMul tile) {
tile.setFacing(value);
}
}
@Override
public ForgeDirection getValue(ItemStack stack) {
return ForgeDirection.NORTH;
}
};
BlockPropertyRegistry.registerProperty(block, property);
BlockPropertyRegistry.registerProperty(Item.getItemFromBlock(block), property);
BlockTest.register();
BlockTestTint.register();
BlockTestTintMul.register();
}
if (GTNHLibConfig.enableTestItems) {
@@ -19,15 +19,6 @@ public class GTNHLibConfig {
@Config.RequiresMcRestart
public static int modelCacheSize;
@Config.Comment("Automatically load model textures based on model files. Disabling may cause models to lose their textures.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
public static boolean autoTextureLoading;
@Config.Comment("Ensures that blocks always return a valid icon for JSON model blocks, by overriding the block icon functions and passing the particle icon.")
@Config.DefaultBoolean(true)
public static boolean modelIconWrapperMixin;
@Config.Comment("Enables various mixins that allow blocks to dynamically change their sound.")
@Config.DefaultBoolean(true)
@Config.RequiresMcRestart
@@ -0,0 +1,11 @@
package com.gtnewhorizon.gtnhlib.api;
/// This class is mixed into {@link net.minecraft.block.Block}, so the model loader can store model information on them.
/// The data must *only* be mutated by the resource reload manager, but can be read by anyone. If your model can only
/// be found by a custom locator, you can implement this yourself and override [#nhlib$isModeled()] to always be true.
public interface BlockModelInfo {
boolean nhlib$isModeled();
void nhlib$setModeled(boolean modeled);
}
@@ -6,11 +6,8 @@ import com.gtnewhorizon.gtnhlib.client.model.BakedModelQuadContext;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
/**
* An interface for {@link Block}s to allow providing custom models for {@link ModelISBRH}. Not necessary for standard
* JSON models. This effectively allows for overwriting {@link ModelISBRH#getModel} without needing a child class, which
* avoids needing a new render ID (and associated mixins).
*/
/// An interface for {@link Block}s to allow providing custom models for {@link ModelISBRH}. Not necessary for standard
/// JSON models. This allows for custom model detection and baking logic.
public interface IBlockModelProvider {
BakedModel getModel(BakedModelQuadContext context);
@@ -36,6 +36,51 @@ public class BlockPropertyRegistry {
private BlockPropertyRegistry() {}
/// Registers the property as normal (see [#registerProperty(Block, BlockProperty)]), and additionally adds it to
/// the respective item (obtained from [Item#getItemFromBlock(Block)]). The item property is assigned irrespective
/// of metadata, and has only one value - the default passed here.
public static <T> void registerBlockItemProperty(Block block, BlockProperty<T> property, T defauld) {
registerProperty(block, property);
registerProperty(Item.getItemFromBlock(block), new BlockProperty<T>() {
@Override
public String getName() {
return property.getName();
}
@Override
public Type getType() {
return property.getType();
}
@Override
public T getValue(ItemStack stack) {
return defauld;
}
@Override
public boolean hasTrait(BlockPropertyTrait trait) {
return trait == BlockPropertyTrait.SupportsStacks;
}
@Override
public String stringify(T t) {
return property.stringify(t);
}
});
}
/// Registers the property on both the block given and its block item.
///
/// @throws IllegalArgumentException if the property doesn't support itemstacks.
public static <T> void registerBlockItemProperty(Block block, BlockProperty<T> property) {
if (!property.hasTrait(BlockPropertyTrait.SupportsStacks))
throw new IllegalArgumentException("BlockItem property should support ItemStacks!");
registerProperty(block, property);
registerProperty(Item.getItemFromBlock(block), property);
}
private static class PropertyMap<K> extends Object2ObjectOpenHashMap<K, Map<String, BlockProperty<?>>> {
public void add(K key, BlockProperty<?> prop) {
@@ -11,6 +11,7 @@ import static net.minecraftforge.client.IItemRenderer.ItemRenderType.INVENTORY;
import java.util.Random;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.item.ItemStack;
@@ -20,11 +21,12 @@ import net.minecraft.world.World;
import net.minecraftforge.client.ForgeHooksClient;
import net.minecraftforge.client.IItemRenderer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.joml.Vector3f;
import org.lwjgl.opengl.GL11;
import com.gtnewhorizon.gtnhlib.api.IBlockModelProvider;
import com.gtnewhorizon.gtnhlib.api.BlockModelInfo;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
import com.gtnewhorizon.gtnhlib.client.model.color.BlockColor;
@@ -33,6 +35,7 @@ import com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry;
import com.gtnewhorizon.gtnhlib.client.renderer.TessellatorManager;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.ModelQuadView;
import com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing;
import com.gtnewhorizon.gtnhlib.core.fml.transformers.BlockIconTransformer;
import com.gtnewhorizons.angelica.api.ThreadSafeISBRH;
import cpw.mods.fml.client.registry.ISimpleBlockRenderingHandler;
@@ -43,9 +46,9 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
public static final ModelISBRH INSTANCE = new ModelISBRH();
/**
* Any blocks using a JSON model should return this for {@link Block#getRenderType()}.
*/
/// Any blocks using a JSON model may return this for [Block#getRenderType()]. However, models are primarily
/// identified via the presence of a blockstate -> model map for the block. If you don't have such a file, you can
/// implement {@link BlockModelInfo} instead, and simply always return true.
public static final int JSON_ISBRH_ID = RenderingRegistry.getNextAvailableRenderId();
private final Random RAND = new Random();
@@ -55,14 +58,6 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
public ModelISBRH() {}
/// For programmatic model selection, see {@link IBlockModelProvider}
@SuppressWarnings("unused")
public BakedModel getModel(BakedModelQuadContext context) {
if (context.getBlockState().getBlock() instanceof IBlockModelProvider selector)
return selector.getModel(context);
return ModelRegistry.getBakedModel(context.getBlockState());
}
@Override
public void renderInventoryBlock(Block block, int meta, int modelId, RenderBlocks renderer) {}
@@ -82,7 +77,7 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
worldContext.blockState = BlockPropertyRegistry.getBlockState(world, x, y, z);
worldContext.random = random;
final BakedModel model = getModel(worldContext);
final BakedModel model = ModelRegistry.getBakedModel(worldContext);
int color = model.getColor(world, x, y, z, block, meta, random);
@@ -243,7 +238,7 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
itemContext.blockState = BlockPropertyRegistry.getBlockState(stack);
itemContext.random = RAND;
final BakedModel model = getModel(itemContext);
final BakedModel model = ModelRegistry.getBakedModel(itemContext);
GL11.glPushMatrix();
GL11.glEnable(GL11.GL_BLEND);
@@ -287,11 +282,9 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
itemContext.reset();
}
/**
* TODO: We need to find a good way to make it so the bound fields are not accounted for but I still want to use
* {@link Block#shouldSideBeRendered}. This is so blocks can define custom culling behavior when needed, but the
* bound fields are not relevant here because JSON models don't listen to them.
*/
/// TODO: We need to find a good way to make it so the bound fields are not accounted for but I still want to use
/// [Block#shouldSideBeRendered]. This is so blocks can define custom culling behavior when needed, but the
/// bound fields are not relevant here because JSON models don't listen to them.
protected boolean shouldSideBeRendered(IBlockAccess world, int x, int y, int z, int side, Block block) {
return block.shouldSideBeRendered(world, x, y, z, side);
}
@@ -418,14 +411,28 @@ public class ModelISBRH implements ISimpleBlockRenderingHandler, IItemRenderer {
}
}
public IIcon getParticleIcon(Block block, @Nullable IBlockAccess world, int x, int y, int z, int meta) {
/// Mirrors the default {@link Block#getIcon(IBlockAccess, int, int, int, int)}, with the exception that the block
/// is also re-fetched. This is because the blockstate construction does world access anyway, overriding just the
/// block is annoying and seems less correct.
///
/// Used in {@link BlockIconTransformer}
public @NotNull IIcon getParticleIcon(@Nullable IBlockAccess world, int x, int y, int z) {
worldContext.world = world;
worldContext.x = x;
worldContext.y = y;
worldContext.z = z;
worldContext.random = RAND;
worldContext.blockState = BlockPropertyRegistry.getBlockState(world, x, y, z);
final var model = getModel(worldContext);
return model.getParticle(worldContext);
final var model = ModelRegistry.getBakedModel(worldContext);
final var ret = model.getParticle(worldContext);
worldContext.reset();
return ret;
}
/// Used in {@link BlockIconTransformer}
@SuppressWarnings("unused")
public @NotNull IIcon getMissingIcon() {
return Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite("missingno");
}
}
@@ -1,8 +1,8 @@
package com.gtnewhorizon.gtnhlib.client.model.loading;
import static com.gtnewhorizon.gtnhlib.GTNHLibConfig.autoTextureLoading;
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;
@@ -11,14 +11,19 @@ import net.minecraft.client.resources.FallbackResourceManager;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.client.resources.IResourceManagerReloadListener;
import net.minecraft.client.resources.IResourcePack;
import net.minecraft.item.Item;
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.client.event.TextureStitchEvent;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.gtnewhorizon.gtnhlib.api.BlockModelInfo;
import com.gtnewhorizon.gtnhlib.api.IBlockModelProvider;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.client.model.BakedModelQuadContext;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.gtnewhorizon.gtnhlib.client.model.baked.BakedModel;
import com.gtnewhorizon.gtnhlib.client.model.state.MissingState;
import com.gtnewhorizon.gtnhlib.client.model.state.StateDeserializer;
@@ -28,6 +33,7 @@ import com.gtnewhorizon.gtnhlib.concurrent.ThreadsafeCache;
import cpw.mods.fml.common.FMLContainerHolder;
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;
@@ -39,7 +45,32 @@ import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
/// beyond excessively complex models being loaded multiple times... add a counter if I'm wrong.
public class ModelRegistry {
public static final Logger MODEL_LOGGER = LogManager.getLogger(ModelRegistry.class);
/// Obtain a model, diverting to a custom pipeline if it exists. See {@link IBlockModelProvider} if you wish to
/// implement said custom pipeline.
public static BakedModel getBakedModel(BakedModelQuadContext context) {
if (context.getBlockState().getBlock() instanceof IBlockModelProvider selector)
return selector.getModel(context);
return getBakedModel(context.getBlockState());
}
/// Getter for {@link BakedModel}s, obtained purely from the default path. We don't want to publicly expose the
/// cache, modders can't be trusted with it :P
public static BakedModel getBakedModel(BlockState state) {
return BLOCKSTATE_MODEL_CACHE.get(state.clone());
}
/// Getter for {@link JSONModel}s - again, normal pipeline, but this grabs them pre-bake. See
/// {@link ModelRegistry#getBakedModel(BlockState)}
public static JSONModel getJSONModel(ResourceLoc.ModelLoc loc) {
return JSON_MODEL_CACHE.get(loc);
}
/// Registers the given mod ID for automatic texture loading. The resource pack attached to this mod will be scanned
/// for model files, and textures from those files will be automatically loaded.
public static void registerModid(String modid) {
ReloadListener.PERMITTED_MODIDS.add(modid);
}
private static final Gson GSON = new GsonBuilder().registerTypeAdapter(StateModelMap.class, new StateDeserializer())
.registerTypeAdapter(JSONModel.class, new ModelDeserializer()).create();
@@ -78,22 +109,6 @@ public class ModelRegistry {
return dough.bake();
}
/// Getter for {@link BakedModel}s. We don't want to publicly expose the cache, modders can't be trusted with it :P
public static BakedModel getBakedModel(BlockState state) {
return BLOCKSTATE_MODEL_CACHE.get(state.clone());
}
/// Getter for {@link JSONModel}s. See {@link ModelRegistry#getBakedModel(BlockState)}
public static JSONModel getJSONModel(ResourceLoc.ModelLoc loc) {
return JSON_MODEL_CACHE.get(loc);
}
/// Registers the given mod ID for automatic texture loading. The resource pack attached to this mod will be scanned
/// for model files, and textures from those files will be automatically loaded.
public static void registerModid(String modid) {
ReloadListener.PERMITTED_MODIDS.add(modid);
}
private static StateModelMap getStateModelMap(Block block) {
final var name = BlockName.fromBlock(block);
final var stateLocation = new ResourceLoc.StateLoc(name.domain, name.name);
@@ -142,14 +157,15 @@ public class ModelRegistry {
return;
}
if (autoTextureLoading) detectAndLoadTextures(manager);
loadModelInfo(manager);
}
private void detectAndLoadTextures(GlobalResourceManager manager) {
private boolean infoMixinFailed = false;
private void loadModelInfo(GlobalResourceManager manager) {
// Scan resource packs for model files
final var domains = manager.nhlib$getDomainResourceManagers();
final var resourcePacks = new ObjectOpenHashSet<IResourcePack>();
for (var entry : domains.entrySet()) {
var files = entry.getValue();
if (files instanceof FallbackResourceManager) {
@@ -157,6 +173,8 @@ public class ModelRegistry {
}
}
// Gather the list of modeled blocks and their textures
final var modeledBlocks = new ObjectOpenHashSet<String>();
final var texturesToLoad = new ObjectArrayList<String>();
for (var pack : resourcePacks) {
if (!(pack instanceof ModelResourcePack mrp)) continue;
@@ -166,17 +184,44 @@ public class ModelRegistry {
&& !PERMITTED_MODIDS.contains(fmlch.getFMLContainer().getModId()))
continue;
final var texture = mrp.nhlib$getReferencedTextures(reader -> GSON.fromJson(reader, JSONModel.class));
texturesToLoad.addAll(texture);
final var info = mrp.nhlib$gatherModelInfo(reader -> GSON.fromJson(reader, JSONModel.class));
modeledBlocks.addAll(info.modeledBlocks());
texturesToLoad.addAll(info.textureNames());
}
GameData.getBlockRegistry().registryObjects.forEach((s, b) -> {
if (!(s instanceof String name)) return;
if (!(b instanceof Block block)) return;
if (!(b instanceof BlockModelInfo modelInfo)) {
if (!infoMixinFailed) {
MODEL_LOGGER.error("Block registry contained a non-block or the info mixin failed!");
MODEL_LOGGER.error("Either you won't notice anything, or all JSON models will stop loading...");
infoMixinFailed = true;
}
return;
}
modelInfo.nhlib$setModeled(modeledBlocks.contains(name));
// We can't shortcut this, since some blocks may manually implement the interface.
if (modelInfo.nhlib$isModeled()) {
final var item = Item.getItemFromBlock(block);
if (item == null) {
MODEL_LOGGER.error(
"Block {} has a null item! Unable to register item model.",
block.getLocalizedName());
return;
}
MinecraftForgeClient.registerItemRenderer(Item.getItemFromBlock(block), ModelISBRH.INSTANCE);
}
});
EventHandler.texturesToLoad = texturesToLoad;
}
}
public static class EventHandler {
private static List<String> texturesToLoad = ObjectLists.emptyList();
private static @NotNull List<String> texturesToLoad = ObjectLists.emptyList();
@SubscribeEvent
@SideOnly(Side.CLIENT)
@@ -184,6 +229,7 @@ public class ModelRegistry {
for (var texture : texturesToLoad) {
event.map.registerIcon(texture.replaceFirst("^minecraft:", ""));
}
texturesToLoad = ObjectLists.emptyList(); // don't need it anymore
}
}
}
@@ -13,7 +13,7 @@ import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
public interface ModelResourcePack {
/// Returns a list of the texture names referenced by models in this resource pack.
List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser);
RPInfo nhlib$gatherModelInfo(Function<Reader, JSONModel> jsonParser);
default List<String> nhlib$getReferencedTextures(Stream<? extends InputStream> files,
Function<Reader, JSONModel> jsonParser) {
@@ -0,0 +1,11 @@
package com.gtnewhorizon.gtnhlib.client.model.loading;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import com.github.bsideup.jabel.Desugar;
/// Tiny holder for data coming from the resourcepack scanning
@Desugar
public record RPInfo(@NotNull List<String> textureNames, @NotNull List<String> modeledBlocks) {}
@@ -1,6 +1,6 @@
package com.gtnewhorizon.gtnhlib.client.model.loading;
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import java.io.IOException;
import java.io.InputStream;
@@ -1,9 +1,9 @@
package com.gtnewhorizon.gtnhlib.client.model.unbaked;
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelDeserializer.ModelElement.Rotation.NOOP;
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.NEG_Y;
import static com.gtnewhorizon.gtnhlib.client.renderer.cel.model.quad.properties.ModelQuadFacing.POS_Y;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.joml.Math.fma;
@@ -6,6 +6,8 @@ import java.util.Set;
import net.minecraft.launchwrapper.Launch;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.launch.GlobalProperties;
import org.spongepowered.asm.service.mojang.MixinServiceLaunchWrapper;
@@ -25,6 +27,8 @@ import cpw.mods.fml.relauncher.IFMLLoadingPlugin;
"com.gtnewhorizon.gtnhlib.client.renderer.CapturingTessellator" })
public class GTNHLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
/// Lifted here so we can safely use it in Mixins. This class should be loaded by the time Mixins fire.
public static final Logger MODEL_LOGGER = LogManager.getLogger("GTNHLib|Models");
private static Boolean isObf;
public GTNHLibCore() {
@@ -46,7 +50,8 @@ public class GTNHLibCore implements IFMLLoadingPlugin, IEarlyMixinLoader {
@Override
public String[] getASMTransformerClass() {
return new String[] { "com.gtnewhorizon.gtnhlib.core.fml.transformers.EventBusSubTransformer" };
return new String[] { "com.gtnewhorizon.gtnhlib.core.fml.transformers.EventBusSubTransformer",
"com.gtnewhorizon.gtnhlib.core.fml.transformers.BlockIconTransformer" };
}
@Override
@@ -0,0 +1,152 @@
package com.gtnewhorizon.gtnhlib.core.fml.transformers;
import static com.gtnewhorizon.gtnhlib.core.fml.transformers.BlockIconTransformer.Blockness.BLOCK;
import static com.gtnewhorizon.gtnhlib.core.fml.transformers.BlockIconTransformer.Blockness.NOT_A_BLOCK;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.GETFIELD;
import static org.objectweb.asm.Opcodes.GETSTATIC;
import static org.objectweb.asm.Opcodes.IFEQ;
import static org.objectweb.asm.Opcodes.ILOAD;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
import static org.objectweb.asm.Type.BOOLEAN_TYPE;
import java.util.HashSet;
import net.minecraft.launchwrapper.IClassTransformer;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldInsnNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.InsnNode;
import org.objectweb.asm.tree.JumpInsnNode;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
public class BlockIconTransformer implements IClassTransformer {
private static final String FIELD_NAME = "nhlib$isModeled";
private static final String BOOL_DESC = BOOLEAN_TYPE.getDescriptor();
private static final String BLOCK_CLASS = "net/minecraft/block/Block";
private static final String ISBRH_CLASS = "com/gtnewhorizon/gtnhlib/client/model/ModelISBRH";
private static final String ISBRH_DESC = "L" + ISBRH_CLASS + ";";
private static final String NEW_WORLD_DESC = "(Lnet/minecraft/world/IBlockAccess;III)Lnet/minecraft/util/IIcon;";
private static final String MISSINGNO_DESC = "()Lnet/minecraft/util/IIcon;";
// These are the targeted calls - a variety of getIcons declared by the Block class
private static final String GI_WORLD = "getIcon(Lnet/minecraft/world/IBlockAccess;IIII)Lnet/minecraft/util/IIcon;";
private static final String GI_SIDE_META = "getIcon(II)Lnet/minecraft/util/IIcon;";
private static final String OBF_SIDE_META = "func_149735_b(II)Lnet/minecraft/util/IIcon;";
private static final String GI_SIDE = "getBlockTextureFromSide(I)Lnet/minecraft/util/IIcon;";
private static final HashSet<String> GETICON_SIGS = new HashSet<>();
static {
GETICON_SIGS.add(GI_WORLD);
GETICON_SIGS.add(GI_SIDE_META);
GETICON_SIGS.add(OBF_SIDE_META);
GETICON_SIGS.add(GI_SIDE);
}
private final ObjectOpenHashSet<String> blockFamily = new ObjectOpenHashSet<>();
public BlockIconTransformer() {
blockFamily.add(BLOCK_CLASS);
}
/// This is what our hook is injecting for the IBlockAccess variant:
/// ```java
/// if (this.nhlib$isModeled) return ModelISBRH.INSTANCE.getParticleIcon(world, x, y, z);
/// ```
/// This is what our hook is injecting for the side:meta and just side variants:
/// ```java
/// if (this.nhlib$isModeled) return ModelISBRH.INSTANCE.getMissingIcon();
/// ```
/// This is because modeled blocks *need* world access to properly get their particle icons. Returning missingno
/// fails loudly, allowing us to find and patch out the incorrect usages.
@Override
public byte[] transform(String name, String transformedName, byte[] basicClass) {
if (basicClass == null) return null;
final var cr = new ClassReader(basicClass);
final var blockness = checkForBlockChild(cr.getClassName(), cr.getSuperName());
if (blockness == NOT_A_BLOCK) {
return basicClass;
}
// Transform the class. We hit:
// - getIcon and overloads (func_149735_b, getBlockTextureFromSide), inserting an early return if needed
// TODO determine if the Block#blockIcon field needs to be redirected
final var cn = new ClassNode();
boolean transformed = false;
if (blockness == BLOCK) {
cn.fields.add(new FieldNode(ACC_PROTECTED, FIELD_NAME, BOOL_DESC, null, 0));
transformed = true;
}
cr.accept(cn, 0);
for (var methodNode : cn.methods) {
transformed |= hookMethod(cn, methodNode);
}
if (transformed) {
final var cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
cn.accept(cw);
return cw.toByteArray();
}
return basicClass;
}
private boolean hookMethod(ClassNode cn, MethodNode mn) {
final var signature = mn.name + mn.desc;
if (!GETICON_SIGS.contains(signature)) return false;
final var injectedHook = new InsnList();
final var endLabel = new LabelNode();
injectedHook.add(new VarInsnNode(ALOAD, 0));
injectedHook.add(new FieldInsnNode(GETFIELD, cn.name, FIELD_NAME, BOOL_DESC));
injectedHook.add(new JumpInsnNode(IFEQ, endLabel));
injectedHook.add(new FieldInsnNode(GETSTATIC, ISBRH_CLASS, "INSTANCE", ISBRH_DESC));
// This call varies based on the node we injected to.
switch (signature) {
case GI_WORLD -> {
injectedHook.add(new VarInsnNode(ALOAD, 1));
injectedHook.add(new VarInsnNode(ILOAD, 2));
injectedHook.add(new VarInsnNode(ILOAD, 3));
injectedHook.add(new VarInsnNode(ILOAD, 4));
injectedHook
.add(new MethodInsnNode(INVOKEVIRTUAL, ISBRH_CLASS, "getParticleIcon", NEW_WORLD_DESC, false));
}
case GI_SIDE_META, OBF_SIDE_META, GI_SIDE -> injectedHook
.add(new MethodInsnNode(INVOKEVIRTUAL, ISBRH_CLASS, "getMissingIcon", MISSINGNO_DESC, false));
default -> throw new RuntimeException("Attempted to hook non-icon method!");
}
injectedHook.add(new InsnNode(ARETURN));
injectedHook.add(endLabel);
mn.instructions.insert(injectedHook);
return true;
}
private Blockness checkForBlockChild(String className, String superName) {
if (BLOCK_CLASS.equals(className)) return BLOCK;
if (blockFamily.contains(superName)) {
blockFamily.add(className);
return Blockness.BLOCK_SUBCLASS;
}
return NOT_A_BLOCK;
}
enum Blockness {
BLOCK,
BLOCK_SUBCLASS,
NOT_A_BLOCK
}
}
@@ -23,19 +23,14 @@ public enum Mixins implements IMixins {
FONT_RENDERER(new MixinBuilder("Font rendering replacements").addClientMixins("MixinFontRenderer")
.setPhase(Phase.EARLY).setApplyIf(() -> GTNHLibConfig.enableFontRendererMixin)),
BLOCK_PROPERTIES_ACCESSORS(Side.COMMON, "MixinTileEntitySkull"),
MODEL_TEXTURE_LOADING(
new MixinBuilder("Automatically load model textures")
.addClientMixins(
"models.FRMAccessor",
"models.MixinFileResourcePack",
"models.MixinFolderResourcePack",
"models.SRRMAccessor")
.setPhase(Phase.EARLY).setApplyIf(() -> GTNHLibConfig.autoTextureLoading)),
MODEL_ICON_WRAPPER(new MixinBuilder(
"Ensures that blocks always return a valid icon for JSON model blocks, by using the particle icon.")
.addClientMixins("models.MixinBlock_IconWrapper")
.addClientMixins("models.MixinEntityDiggingFX_IconWrapper").setPhase(Phase.EARLY)
.setApplyIf(() -> GTNHLibConfig.modelIconWrapperMixin)),
MODEL_TEXTURE_LOADING(new MixinBuilder("Automatically load models, textures, and metadata.").addClientMixins(
"models.FRMAccessor",
"models.MixinBlock_ModelInfo",
"models.MixinEffectRenderer_FixParticleIcons",
"models.MixinFileResourcePack",
"models.MixinFolderResourcePack",
"models.MixinRenderBlocks",
"models.SRRMAccessor").setPhase(Phase.EARLY).setApplyIf(() -> true)),
MODEL_ITEM_RENDERER(new MixinBuilder("Restore origin pivot before modifier").addClientMixins("models.MixinModelFHC")
.setPhase(Phase.EARLY)),
DYNAMIC_BLOCK_SOUNDS(new MixinBuilder("Dynamic block sounds")
@@ -1,76 +0,0 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.IIconRegister;
import net.minecraft.util.IIcon;
import net.minecraft.world.IBlockAccess;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import cpw.mods.fml.client.FMLClientHandler;
import cpw.mods.fml.client.registry.RenderingRegistry;
@Mixin(Block.class)
public abstract class MixinBlock_IconWrapper {
@Shadow
public abstract int getRenderType();
@Shadow
protected IIcon blockIcon;
@WrapMethod(method = "getIcon(Lnet/minecraft/world/IBlockAccess;IIII)Lnet/minecraft/util/IIcon;")
private IIcon nhlib$wrapGetIcon(IBlockAccess world, int x, int y, int z, int side, Operation<IIcon> original) {
return RenderingRegistry.instance().blockRenderers.get(getRenderType()) instanceof ModelISBRH
? nhlib$getParticleIcon(world, x, y, z, world.getBlockMetadata(x, y, z))
: original.call(world, x, y, z, side);
}
@WrapMethod(method = "getIcon(II)Lnet/minecraft/util/IIcon;")
private IIcon nhlib$wrapGetIcon(int side, int meta, Operation<IIcon> original) {
return RenderingRegistry.instance().blockRenderers.get(getRenderType()) instanceof ModelISBRH
? nhlib$getParticleIcon(null, 0, 0, 0, meta)
: original.call(side, meta);
}
@WrapMethod(method = "func_149735_b")
private IIcon nhlib$wrapGetIconObf(int side, int meta, Operation<IIcon> original) {
return RenderingRegistry.instance().blockRenderers.get(getRenderType()) instanceof ModelISBRH
? nhlib$getParticleIcon(null, 0, 0, 0, meta)
: original.call(side, meta);
}
@WrapMethod(method = "getBlockTextureFromSide")
private IIcon nhlib$wrapGetIcon(int side, Operation<IIcon> original) {
// Set the blockIcon here so anything outside accessing it with ATs still gets the correct icon.
// Can't do it in registericons because the icon isn't baked or somethimg, it crashes.
return RenderingRegistry.instance().blockRenderers.get(getRenderType()) instanceof ModelISBRH
? nhlib$getParticleIcon(null, 0, 0, 0, 0)
: original.call(side);
}
@WrapMethod(method = "registerBlockIcons")
private void nhlib$setField(IIconRegister reg, Operation<Void> original) {
original.call(reg);
if (RenderingRegistry.instance().blockRenderers.get(getRenderType()) instanceof ModelISBRH
&& blockIcon == null) {
blockIcon = Minecraft.getMinecraft().getTextureMapBlocks().getAtlasSprite("missingno");
}
}
@Unique
private IIcon nhlib$getParticleIcon(@Nullable IBlockAccess world, int x, int y, int z, int meta) {
IIcon particle = ModelISBRH.INSTANCE.getParticleIcon((Block) (Object) this, world, x, y, z, meta);
return particle == null
? FMLClientHandler.instance().getClient().getTextureMapBlocks().registerIcon("missingno")
: particle;
}
}
@@ -0,0 +1,28 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.block.Block;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import com.gtnewhorizon.gtnhlib.api.BlockModelInfo;
@Mixin(Block.class)
public class MixinBlock_ModelInfo implements BlockModelInfo {
/// This is a shadowed field, not a unique one, because we injected it with ASM. See
/// {@link com.gtnewhorizon.gtnhlib.core.fml.transformers.BlockIconTransformer}
@SuppressWarnings("MixinAnnotationTarget")
@Shadow(remap = false)
private boolean nhlib$isModeled = false;
@Override
public boolean nhlib$isModeled() {
return nhlib$isModeled;
}
@Override
public void nhlib$setModeled(boolean modeled) {
nhlib$isModeled = modeled;
}
}
@@ -0,0 +1,48 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.block.Block;
import net.minecraft.client.particle.EffectRenderer;
import net.minecraft.client.particle.EntityDiggingFX;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import com.gtnewhorizon.gtnhlib.api.BlockModelInfo;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
@Mixin(EffectRenderer.class)
public class MixinEffectRenderer_FixParticleIcons {
@Shadow
protected World worldObj;
@ModifyExpressionValue(
method = "addBlockDestroyEffects",
at = @At(
value = "NEW",
target = "(Lnet/minecraft/world/World;DDDDDDLnet/minecraft/block/Block;I)Lnet/minecraft/client/particle/EntityDiggingFX;"))
private EntityDiggingFX nhlib$fixBreakIcon(EntityDiggingFX original, @Local(argsOnly = true) Block block,
@Local(ordinal = 0, argsOnly = true) int x, @Local(ordinal = 1, argsOnly = true) int y,
@Local(ordinal = 2, argsOnly = true) int z) {
if (((BlockModelInfo) block).nhlib$isModeled())
original.setParticleIcon(ModelISBRH.INSTANCE.getParticleIcon(worldObj, x, y, z));
return original;
}
@ModifyExpressionValue(
method = "addBlockHitEffects",
at = @At(
value = "NEW",
target = "(Lnet/minecraft/world/World;DDDDDDLnet/minecraft/block/Block;I)Lnet/minecraft/client/particle/EntityDiggingFX;"))
private EntityDiggingFX nhlib$fixHitIcon(EntityDiggingFX original, @Local(ordinal = 0) Block block,
@Local(ordinal = 0, argsOnly = true) int x, @Local(ordinal = 1, argsOnly = true) int y,
@Local(ordinal = 2, argsOnly = true) int z) {
if (((BlockModelInfo) block).nhlib$isModeled())
original.setParticleIcon(ModelISBRH.INSTANCE.getParticleIcon(worldObj, x, y, z));
return original;
}
}
@@ -1,35 +0,0 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.block.Block;
import net.minecraft.client.particle.EntityDiggingFX;
import net.minecraft.util.IIcon;
import net.minecraft.world.World;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import cpw.mods.fml.client.registry.RenderingRegistry;
@Mixin(EntityDiggingFX.class)
public class MixinEntityDiggingFX_IconWrapper {
@WrapOperation(
method = "<init>(Lnet/minecraft/world/World;DDDDDDLnet/minecraft/block/Block;II)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/block/Block;getIcon(II)Lnet/minecraft/util/IIcon;"))
private IIcon nhlib$useWorldIcon(Block block, int side, int meta, Operation<IIcon> original, World world, double x,
double y, double z, double mx, double my, double mz, Block passedBlock, int passedMeta, int passedSide) {
if (RenderingRegistry.instance().blockRenderers.get(block.getRenderType()) instanceof ModelISBRH) {
int bx = (int) Math.floor(x);
int by = (int) Math.floor(y);
int bz = (int) Math.floor(z);
return block.getIcon(world, bx, by, bz, side);
}
return original.call(block, side, meta);
}
}
@@ -1,10 +1,11 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.zip.ZipFile;
@@ -16,6 +17,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelResourcePack;
import com.gtnewhorizon.gtnhlib.client.model.loading.RPInfo;
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
import it.unimi.dsi.fastutil.objects.ObjectLists;
@@ -32,21 +34,25 @@ public abstract class MixinFileResourcePack extends AbstractResourcePack impleme
}
@Override
public List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser) {
public RPInfo nhlib$gatherModelInfo(Function<Reader, JSONModel> jsonParser) {
List<String> textures;
final var models = new ArrayList<String>();
try {
var zip = getResourcePackZipFile();
final var jsons = zip.stream().filter(ze -> {
var jsons = zip.stream().filter(ze -> {
final var name = ze.getName();
final var parts = name.split("/");
// Make sure it's long enough (assets/<domain>/<subdomain>/something.json), make sure the subdomain is
// "models", make sure it's a file, and make sure it's a JSON
// If it's a blockstate file, record the block and move on
if (parts.length < 4) return false;
if (!parts[2].equals("models")) return false;
if (ze.isDirectory()) return false;
return name.endsWith(".json");
if (!name.endsWith(".json")) return false;
if (parts[2].equals("blockstates")) { // file is assets/<domain>/blockstates/someblock.json
models.add(parts[1] + ":" + parts[3].split("\\.")[0]);
return false;
} else return parts[2].equals("models"); // file is assets/<domain>/models/someblock.json
}).map(ze -> {
try {
return zip.getInputStream(ze);
} catch (IOException e) {
@@ -54,13 +60,14 @@ public abstract class MixinFileResourcePack extends AbstractResourcePack impleme
}
});
return nhlib$getReferencedTextures(jsons, jsonParser);
textures = nhlib$getReferencedTextures(jsons, jsonParser);
} catch (Exception e) {
MODEL_LOGGER.warn("Failed to walk resource pack {}", this);
MODEL_LOGGER.warn(e);
textures = ObjectLists.emptyList();
}
return ObjectLists.emptyList();
return new RPInfo(textures, models);
}
}
@@ -1,6 +1,6 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import static com.gtnewhorizon.gtnhlib.client.model.loading.ModelRegistry.MODEL_LOGGER;
import static com.gtnewhorizon.gtnhlib.core.GTNHLibCore.MODEL_LOGGER;
import static java.nio.file.Files.walk;
import java.io.File;
@@ -8,6 +8,7 @@ import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.Reader;
import java.nio.file.FileVisitOption;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
@@ -17,6 +18,7 @@ import net.minecraft.client.resources.FolderResourcePack;
import org.spongepowered.asm.mixin.Mixin;
import com.gtnewhorizon.gtnhlib.client.model.loading.ModelResourcePack;
import com.gtnewhorizon.gtnhlib.client.model.loading.RPInfo;
import com.gtnewhorizon.gtnhlib.client.model.unbaked.JSONModel;
import it.unimi.dsi.fastutil.objects.ObjectLists;
@@ -30,7 +32,10 @@ public abstract class MixinFolderResourcePack extends AbstractResourcePack imple
}
@Override
public List<String> nhlib$getReferencedTextures(Function<Reader, JSONModel> jsonParser) {
public RPInfo nhlib$gatherModelInfo(Function<Reader, JSONModel> jsonParser) {
List<String> textures;
final var models = new ArrayList<String>();
try (var files = walk(resourcePackFile.toPath(), FileVisitOption.FOLLOW_LINKS)) {
final var jsons = files.filter(p -> {
@@ -39,10 +44,16 @@ public abstract class MixinFolderResourcePack extends AbstractResourcePack imple
// Make sure it's long enough (<domain>/<subdomain>/something.json), make sure the subdomain is
// "models", make sure it's a file, and make sure it's a JSON
if (path.getNameCount() < 3) return false;
if (!path.getName(1).toString().equals("models")) return false;
if (path.getNameCount() != 3) return false;
if (!path.toFile().isFile()) return false;
return path.endsWith(".json");
final var filename = path.getFileName().toString();
if (!filename.endsWith(".json")) return false;
final var subdomain = path.getName(1).toString();
if (subdomain.equals("blockstates")) {
models.add(path.getName(0) + ":" + filename.split("\\.")[0]);
return false;
} else return subdomain.equals("models");
}).map(p -> {
try {
return new FileInputStream(p.toFile());
@@ -51,12 +62,13 @@ public abstract class MixinFolderResourcePack extends AbstractResourcePack imple
}
});
return nhlib$getReferencedTextures(jsons, jsonParser);
textures = nhlib$getReferencedTextures(jsons, jsonParser);
} catch (Exception e) {
MODEL_LOGGER.warn("Failed to walk resource pack {}", this);
MODEL_LOGGER.warn(e);
textures = ObjectLists.emptyList();
}
return ObjectLists.emptyList();
return new RPInfo(textures, models);
}
}
@@ -0,0 +1,38 @@
package com.gtnewhorizon.gtnhlib.mixins.early.models;
import net.minecraft.block.Block;
import net.minecraft.client.renderer.RenderBlocks;
import net.minecraft.world.IBlockAccess;
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.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import com.gtnewhorizon.gtnhlib.api.BlockModelInfo;
import com.gtnewhorizon.gtnhlib.client.model.ModelISBRH;
import com.llamalad7.mixinextras.sugar.Local;
@Mixin(RenderBlocks.class)
public abstract class MixinRenderBlocks {
@Shadow
public IBlockAccess blockAccess;
@Inject(
method = "renderBlockByRenderType",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/client/renderer/RenderBlocks;setRenderBoundsFromBlock(Lnet/minecraft/block/Block;)V",
shift = At.Shift.AFTER),
cancellable = true)
private void nhlib$injectModelISBRH(Block block, int x, int y, int z, CallbackInfoReturnable<Boolean> cir,
@Local(name = "l") int renderType) {
if (((BlockModelInfo) block).nhlib$isModeled()) {
cir.setReturnValue(
ModelISBRH.INSTANCE
.renderWorldBlock(blockAccess, x, y, z, block, renderType, (RenderBlocks) (Object) this));
}
}
}
@@ -1,20 +1,41 @@
package com.gtnewhorizon.gtnhlib.test.block;
import static com.gtnewhorizon.gtnhlib.client.model.ModelISBRH.JSON_ISBRH_ID;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.util.MathHelper;
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 BlockTest extends Block {
/// This default blockstate
private final AbstractDirectionBlockProperty FACING_PROP = (AbstractDirectionBlockProperty) DirectionBlockProperty
.facing();
public BlockTest() {
super(Material.wood);
}
public static void register() {
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);
}
@Override
public boolean isOpaqueCube() {
return false;
@@ -22,12 +43,10 @@ public class BlockTest extends Block {
@Override
public void onBlockPlacedBy(World worldIn, int x, int y, int z, EntityLivingBase placer, ItemStack itemIn) {
int meta = MathHelper.floor_double((double) (placer.rotationYaw * 4.0F / 360.0F) + 0.5D) & 3;
worldIn.setBlockMetadataWithNotify(x, y, z, meta, 2);
}
var dir = DirectionUtil.yawToDirection(placer.rotationYaw);
@Override
public int getRenderType() {
return JSON_ISBRH_ID;
/// 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);
}
}
@@ -1,18 +1,14 @@
package com.gtnewhorizon.gtnhlib.test.block;
import static com.gtnewhorizon.gtnhlib.client.model.ModelISBRH.JSON_ISBRH_ID;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.item.ItemStack;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import org.jetbrains.annotations.NotNull;
import com.gtnewhorizon.gtnhlib.client.model.color.IBlockColor;
import cpw.mods.fml.common.registry.GameRegistry;
public class BlockTestTint extends Block implements IBlockColor {
public BlockTestTint() {
@@ -20,46 +16,28 @@ public class BlockTestTint extends Block implements IBlockColor {
setHardness(2f);
}
public static void register() {
final var block = new BlockTestTint();
block.setBlockName("model_test_tint");
GameRegistry.registerBlock(block, "model_test_tint");
}
@Override
public boolean isOpaqueCube() {
return false;
}
@Override
public int onBlockPlaced(@NotNull World worldIn, int x, int y, int z, int side, float subX, float subY, float subZ,
int meta) {
// Face NORTH if placed up or down
final var s = ForgeDirection.getOrientation(side);
if (s == ForgeDirection.UP || s == ForgeDirection.DOWN) return 2;
// Face the placed side
return side - 2;
}
@Override
public int getRenderType() {
return JSON_ISBRH_ID;
}
@Override
public int colorMultiplier(IBlockAccess world, int x, int y, int z, int tintIndex) {
switch (tintIndex) {
case 0:
return 0xFF0000; // red
case 1:
return 0x00FF00; // green
case 2:
return 0x0000FF; // blue
case 3:
return 0xFFFF00; // yellow
case 4:
return 0xFF00FF; // purple
case 5:
return 0x00FFFF; // cyan
default:
return 0xFFFFFF;// white
}
return switch (tintIndex) {
case 0 -> 0xFF0000; // red
case 1 -> 0x00FF00; // green
case 2 -> 0x0000FF; // blue
case 3 -> 0xFFFF00; // yellow
case 4 -> 0xFF00FF; // purple
case 5 -> 0x00FFFF; // cyan
default -> 0xFFFFFF;// white
};
}
@Override
@@ -1,20 +1,22 @@
package com.gtnewhorizon.gtnhlib.test.block;
import static com.gtnewhorizon.gtnhlib.client.model.ModelISBRH.JSON_ISBRH_ID;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.MathHelper;
import net.minecraft.world.IBlockAccess;
import net.minecraft.world.World;
import net.minecraftforge.common.util.ForgeDirection;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockPropertyTrait;
import com.gtnewhorizon.gtnhlib.blockstate.core.BlockState;
import com.gtnewhorizon.gtnhlib.blockstate.properties.DirectionBlockProperty;
import com.gtnewhorizon.gtnhlib.blockstate.registry.BlockPropertyRegistry;
import com.gtnewhorizon.gtnhlib.client.model.color.IBlockColor;
import com.gtnewhorizon.gtnhlib.util.DirectionUtil;
import cpw.mods.fml.common.registry.GameRegistry;
public class BlockTestTintMul extends Block implements IBlockColor {
@@ -23,6 +25,52 @@ public class BlockTestTintMul extends Block implements IBlockColor {
setHardness(2f);
}
public static void register() {
final var tintMulTestBlock = new BlockTestTintMul();
tintMulTestBlock.setBlockName("model_test_tint_mul");
GameRegistry.registerBlock(tintMulTestBlock, "model_test_tint_mul");
GameRegistry.registerTileEntity(TileTestTintMul.class, "tile_test_tint_mul");
// Here's an example of a custom, TE-based property
final var property = new DirectionBlockProperty() {
@Override
public String getName() {
return "facing";
}
/// This DOES support stacks, otherwise the item wouldn't have a facing, and thus wouldn't have a model.
@Override
public boolean hasTrait(BlockPropertyTrait trait) {
return switch (trait) {
case SupportsWorld, WorldMutable, StackMutable, SupportsStacks -> true;
default -> false;
};
}
@Override
public ForgeDirection getValue(IBlockAccess world, int x, int y, int z) {
TileEntity te = world.getTileEntity(x, y, z);
if (te instanceof TileTestTintMul tile) return tile.getFacing();
return ForgeDirection.NORTH;
}
@Override
public void setValue(World world, int x, int y, int z, ForgeDirection value) {
TileEntity te = world.getTileEntity(x, y, z);
if (te instanceof TileTestTintMul tile) tile.setFacing(value);
}
/// This needs to be implemented for the stack support to work.
@Override
public ForgeDirection getValue(ItemStack stack) {
return ForgeDirection.NORTH;
}
};
BlockPropertyRegistry.registerBlockItemProperty(tintMulTestBlock, property);
}
@Override
public boolean isOpaqueCube() {
return false;
@@ -30,25 +78,12 @@ public class BlockTestTintMul extends Block implements IBlockColor {
@Override
public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase player, ItemStack itemIn) {
super.onBlockPlacedBy(world, x, y, z, player, itemIn);
int heading = MathHelper.floor_double(player.rotationYaw * 4.0F / 360.0F + 0.5D) & 3;
ForgeDirection facing = getDirectionForHeading(heading);
BlockState state = BlockPropertyRegistry.getBlockState(world, x, y, z);
state.setPropertyValue("facing", facing);
state.setPropertyValue("facing", DirectionUtil.yawToDirection(player.rotationYaw));
state.place(world, x, y, z);
state.close();
}
private ForgeDirection getDirectionForHeading(int heading) {
return switch (heading) {
case 0 -> ForgeDirection.NORTH;
case 1 -> ForgeDirection.EAST;
case 2 -> ForgeDirection.SOUTH;
case 3 -> ForgeDirection.WEST;
default -> ForgeDirection.NORTH;
};
}
@Override
public boolean hasTileEntity(int meta) {
return true;
@@ -59,29 +94,17 @@ public class BlockTestTintMul extends Block implements IBlockColor {
return new TileTestTintMul();
}
@Override
public int getRenderType() {
return JSON_ISBRH_ID;
}
@Override
public int colorMultiplier(IBlockAccess world, int x, int y, int z, int tintIndex) {
switch (tintIndex) {
case 0:
return 0xFF0000; // red
case 1:
return 0x00FF00; // green
case 2:
return 0x0000FF; // blue
case 3:
return 0xFFFF00; // yellow
case 4:
return 0xFF00FF; // purple
case 5:
return 0x00FFFF; // cyan
default:
return 0xFFFFFF;// white
}
return switch (tintIndex) {
case 0 -> 0xFF0000; // red
case 1 -> 0x00FF00; // green
case 2 -> 0x0000FF; // blue
case 3 -> 0xFFFF00; // yellow
case 4 -> 0xFF00FF; // purple
case 5 -> 0x00FFFF; // cyan
default -> 0xFFFFFF;// white
};
}
@Override
@@ -0,0 +1,15 @@
package com.gtnewhorizon.gtnhlib.util;
import net.minecraftforge.common.util.ForgeDirection;
public class DirectionUtil {
public static ForgeDirection yawToDirection(float yaw) {
return switch (Math.round(MathUtil.mod(yaw, 360) / 360 * 4)) {
case 1 -> ForgeDirection.EAST;
case 2 -> ForgeDirection.SOUTH;
case 3 -> ForgeDirection.WEST;
default -> ForgeDirection.NORTH;
};
}
}
@@ -0,0 +1,14 @@
package com.gtnewhorizon.gtnhlib.util;
public class MathUtil {
/// I think this does the [Math#floorMod(int, int)] thing to floats. I'm sure it's implemented *somewhere*, but I
/// don't know where.
public static float mod(float a, float b) {
if (a < 0) {
final var ret = b - (-a % b);
return ret == b ? 0 : ret; // when -a == b, should return 0, not b
}
return a % b;
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
public cpw.mods.fml.client.registry.RenderingRegistry blockRenderers
public net.minecraft.client.renderer.Tessellator field_78405_h # rawBuffer
public net.minecraft.client.renderer.Tessellator field_78406_i # vertexCount
public net.minecraft.client.renderer.Tessellator field_78403_j # textureU
@@ -22,4 +23,4 @@ public net.minecraft.client.renderer.Tessellator field_78410_t # isColorDisabled
public net.minecraft.client.renderer.Tessellator func_78379_d()V # void reset()
public net.minecraft.client.renderer.Tessellator field_78394_d # byteBuffer
public net.minecraft.command.CommandHandler field_71562_a # commandMap
public cpw.mods.fml.client.registry.RenderingRegistry blockRenderers
public net.minecraft.util.RegistrySimple field_82596_a # registryObjects
@@ -1,8 +1,8 @@
{
"variants": {
"meta=0": { "model": "gtnhlib:blocks/old_stairs", "y": 270, "uvlock": true },
"meta=1": { "model": "gtnhlib:blocks/old_stairs", "y": 180, "uvlock": true },
"meta=2": { "model": "gtnhlib:blocks/old_stairs", "y": 90, "uvlock": true },
"meta=3": { "model": "gtnhlib:blocks/old_stairs", "y": 0, "uvlock": true }
"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,5 @@
{
"variants": {
"meta=0": { "model": "gtnhlib:blocks/model_test_tint", "y": 270 },
"meta=1": { "model": "gtnhlib:blocks/model_test_tint", "y": 90 },
"meta=2": { "model": "gtnhlib:blocks/model_test_tint", "y": 0 },
"meta=3": { "model": "gtnhlib:blocks/model_test_tint", "y": 180 }
"": { "model": "gtnhlib:blocks/model_test_tint" }
}
}
@@ -1,3 +1,8 @@
# Test block names
tile.model_test.name=Test Block
tile.model_test_tint.name=Test Block (Tint)
tile.model_test_tint_mul.name=Test Block (Tint Mul)
# Configuration
fml.configgui.tooltip.default=[default: %s]
fml.configgui.tooltip.defaultNumeric=[range: %2$s ~ %3$s, default: %1$s]
@@ -116,4 +121,4 @@ gtnhlib.chat.rpupdater.info.header=§2Pack info for §3%s§2:
gtnhlib.chat.rpupdater.info.no_updater=No updater metadata found for this pack.
gtnhlib.chat.rpupdater.info.updater_name=Updater name: §7%s
gtnhlib.chat.rpupdater.info.version=Version: §7%s (GTNH %s)
gtnhlib.chat.rpupdater.info.source=Source: §7%s/%s
gtnhlib.chat.rpupdater.info.source=Source: §7%s/%s
Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 B

After

Width:  |  Height:  |  Size: 244 B