Containers and GUIs

From Minecraft Forge
Jump to: navigation, search

How-To Icon.png

This is a How-To guide or Tutorial detailing a practice or process for Minecraft Forge or related software.

Prerequisites

  • An understanding of java
  • An understanding of vanilla minecraft and a basic understanding of forge
  • A Block
  • A TileEntity for said block that implements IInventory
  • A GUI image (see the /gui/ folder in minecraft.jar for references)

In this tutorial, we will create a block that holds a stack of items (a "tiny chest").

Example TileEntity and Block classes (for reference)

TileEntityTiny.java

<source lang=java> package tco.tiny;

import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.tileentity.TileEntity;

public class TileEntityTiny extends TileEntity implements IInventory {

private ItemStack[] inv;

public TileEntityTiny() { inv = new ItemStack[9]; }

@Override public int getSizeInventory() { return inv.length; }

@Override public ItemStack getStackInSlot(int slot) { return inv[slot]; }

@Override public void setInventorySlotContents(int slot, ItemStack stack) { inv[slot] = stack; if (stack != null && stack.stackSize > getInventoryStackLimit()) { stack.stackSize = getInventoryStackLimit(); } }

@Override public ItemStack decrStackSize(int slot, int amt) { ItemStack stack = getStackInSlot(slot); if (stack != null) { if (stack.stackSize <= amt) { setInventorySlotContents(slot, null); } else { stack = stack.splitStack(amt); if (stack.stackSize == 0) { setInventorySlotContents(slot, null); } } } return stack; }

@Override public ItemStack getStackInSlotOnClosing(int slot) { ItemStack stack = getStackInSlot(slot); if (stack != null) { setInventorySlotContents(slot, null); } return stack; }

@Override public int getInventoryStackLimit() { return 64; }

@Override public boolean isUseableByPlayer(EntityPlayer player) { return worldObj.getTileEntity(xCoord, yCoord, zCoord) == this && player.getDistanceSq(xCoord + 0.5, yCoord + 0.5, zCoord + 0.5) < 64; }

@Override public void openInventory() { }

@Override public void closeInventory() { }

@Override public void readFromNBT(NBTTagCompound tagCompound) { super.readFromNBT(tagCompound);

NBTTagList tagList = tagCompound.getTagList("Inventory", 10); for (int i = 0; i < tagList.tagCount(); i++) { NBTTagCompound tag = (NBTTagCompound) tagList.getCompoundTagAt(i); byte slot = tag.getByte("Slot"); if (slot >= 0 && slot < inv.length) { inv[slot] = ItemStack.loadItemStackFromNBT(tag); } } }

@Override public void writeToNBT(NBTTagCompound tagCompound) { super.writeToNBT(tagCompound);

NBTTagList itemList = new NBTTagList(); for (int i = 0; i < inv.length; i++) { ItemStack stack = inv[i]; if (stack != null) { NBTTagCompound tag = new NBTTagCompound(); tag.setByte("Slot", (byte) i); stack.writeToNBT(tag); itemList.appendTag(tag); } } tagCompound.setTag("Inventory", itemList); }

@Override public boolean hasCustomInventoryName() { return true; }

@Override public String getInventoryName() { return "tco.tileentitytiny"; }

@Override public boolean isItemValidForSlot(int slot, ItemStack itemStack) { return true; } } </source>

BlockTiny.java

<source lang=java> package tco.tiny;

import java.util.Random;

import net.minecraft.block.BlockContainer; import net.minecraft.block.material.Material; import net.minecraft.creativetab.CreativeTabs; import net.minecraft.entity.item.EntityItem; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World;

public class BlockTiny extends BlockContainer {

protected BlockTiny (int id) { super(id, Material.wood); setHardness(2.0F); setResistance(5.0F); setBlockName("blockTiny"); setCreativeTab(CreativeTabs.tabDecorations); }

@Override public boolean onBlockActivated(World world, int x, int y, int z, EntityPlayer player, int metadata, float what, float these, float are) { TileEntity tileEntity = world.getBlockTileEntity(x, y, z); if (tileEntity == null || player.isSneaking()) { return false; }

       //code to open gui explained later
       player.openGui(Tiny.instance, 0, world, x, y, z);

return true; }

@Override public void breakBlock(World world, int x, int y, int z, int par5, int par6) { dropItems(world, x, y, z); super.breakBlock(world, x, y, z, par5, par6); }

private void dropItems(World world, int x, int y, int z){ Random rand = new Random();

TileEntity tileEntity = world.getBlockTileEntity(x, y, z); if (!(tileEntity instanceof IInventory)) { return; } IInventory inventory = (IInventory) tileEntity;

for (int i = 0; i < inventory.getSizeInventory(); i++) { ItemStack item = inventory.getStackInSlot(i);

if (item != null && item.stackSize > 0) { float rx = rand.nextFloat() * 0.8F + 0.1F; float ry = rand.nextFloat() * 0.8F + 0.1F; float rz = rand.nextFloat() * 0.8F + 0.1F;

EntityItem entityItem = new EntityItem(world, x + rx, y + ry, z + rz, new ItemStack(item.itemID, item.stackSize, item.getItemDamage()));

if (item.hasTagCompound()) { entityItem.getEntityItem().setTagCompound((NBTTagCompound) item.getTagCompound().copy()); }

float factor = 0.05F; entityItem.motionX = rand.nextGaussian() * factor; entityItem.motionY = rand.nextGaussian() * factor + 0.2F; entityItem.motionZ = rand.nextGaussian() * factor; world.spawnEntityInWorld(entityItem); item.stackSize = 0; } } }

@Override public TileEntity createNewTileEntity(World world) { return new TileEntityTiny(); }

} </source>

Creating the container

A container is what connects the inventories of the player and tileentity to the GUI. In the class definition, we will define the position on-screen and contents of each slot. We will also define Shift-click behaviour for the player's convenience. Take note of:

  • Slot(IInventory, int, int, int)
  • addSlotToContainer(Slot)
  • transferStackInSlot(EntityPlayer, int)

<source lang=java> package tco.tiny;

import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.inventory.Container; import net.minecraft.inventory.Slot; import net.minecraft.item.ItemStack;

public class ContainerTiny extends Container {

protected TileEntityTiny tileEntity;

public ContainerTiny (InventoryPlayer inventoryPlayer, TileEntityTiny te){ tileEntity = te;

//the Slot constructor takes the IInventory and the slot number in that it binds to //and the x-y coordinates it resides on-screen for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { addSlotToContainer(new Slot(tileEntity, j + i * 3, 62 + j * 18, 17 + i * 18)); } }

//commonly used vanilla code that adds the player's inventory bindPlayerInventory(inventoryPlayer); }

@Override public boolean canInteractWith(EntityPlayer player) { return tileEntity.isUseableByPlayer(player); }


protected void bindPlayerInventory(InventoryPlayer inventoryPlayer) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 9; j++) { addSlotToContainer(new Slot(inventoryPlayer, j + i * 9 + 9, 8 + j * 18, 84 + i * 18)); } }

for (int i = 0; i < 9; i++) { addSlotToContainer(new Slot(inventoryPlayer, i, 8 + i * 18, 142)); } }

@Override public ItemStack transferStackInSlot(EntityPlayer player, int slot) { ItemStack stack = null; Slot slotObject = (Slot) inventorySlots.get(slot);

//null checks and checks if the item can be stacked (maxStackSize > 1) if (slotObject != null && slotObject.getHasStack()) { ItemStack stackInSlot = slotObject.getStack(); stack = stackInSlot.copy();

//merges the item into player inventory since its in the tileEntity if (slot < tileEntity.getSizeInventory()) { if (!this.mergeItemStack(stackInSlot, tileEntity.getSizeInventory(), 36+tileEntity.getSizeInventory(), true)) { return null; } } //places it into the tileEntity is possible since its in the player inventory else if (!this.mergeItemStack(stackInSlot, 0, tileEntity.getSizeInventory(), false)) { return null; }

if (stackInSlot.stackSize == 0) { slotObject.putStack(null); } else { slotObject.onSlotChanged(); }

if (stackInSlot.stackSize == stack.stackSize) { return null; } slotObject.onPickupFromSlot(player, stackInSlot); } return stack; } } </source> Now we add the transferStackInSlot method. Without this method properly overridden, Shift-clicking will almost always cause massive errors. This bit of code is quite confusing. There are 2 important parts of the code we must understand: the "slot" and the method mergeItemStack.

First, the "slot" parameter is the slot number of the Container. Each call to the addSlotToContainer method increases the number of slots in the Container by 1. They can be accessed in the order that we added them. i.e. slot 0 would be the single inventory slot of TileEntityTiny.

Secondly, here's my (guessed) documentation of the mergeItemStack method: <source lang=java> /**

* @param itemStack ItemStack to merge into inventory
* @param start minimum slot to attempt fill
* @param end maximum slot to attempt fill
* @param backwards go backwards
* @return true if stacks merged successfully
*/

public boolean mergeItemStack(itemStack, start, end, backwards) </source> Basically, this method will attempt to place the given itemStack within the Container slots between start and end. It will try to fill from end to start if backwards=true, and vice versa. You can use the transferStackInSlot method for "intelligent" shift-clicking, like putting smeltable items in the top slot of a furnance and fuel in the bottom slot.

Drawing the GUI

Now we will create the Gui for the Container. We won't mess around that much with OpenGL because it is big and scary. <source lang=java> package tco.tiny;

import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.entity.player.InventoryPlayer; import net.minecraft.util.StatCollector;

import org.lwjgl.opengl.GL11;

public class GuiTiny extends GuiContainer {

       public GuiTiny (InventoryPlayer inventoryPlayer,
                       TileEntityTiny tileEntity) {
               //the container is instanciated and passed to the superclass for handling
               super(new ContainerTiny(inventoryPlayer, tileEntity));
       }
       @Override
       protected void drawGuiContainerForegroundLayer(int param1, int param2) {
               //draw text and stuff here
               //the parameters for drawString are: string, x, y, color
               fontRenderer.drawString("Tiny", 8, 6, 4210752);
               //draws "Inventory" or your regional equivalent
               fontRenderer.drawString(StatCollector.translateToLocal("container.inventory"), 8, ySize - 96 + 2, 4210752);
       }
       @Override
       protected void drawGuiContainerBackgroundLayer(float par1, int par2,
                       int par3) {
               //draw your Gui here, only thing you need to change is the path
               int texture = mc.renderEngine.getTexture("/gui/trap.png");
               GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
               this.mc.renderEngine.bindTexture(texture);
               int x = (width - xSize) / 2;
               int y = (height - ySize) / 2;
               this.drawTexturedModalRect(x, y, 0, 0, xSize, ySize);
       }

} </source>

Opening the GUI

Now we can open the Gui. First, you need a class that implements IGuiHandler. There are 2 methods you need: getClientGuiElement and getServerGuiElement. Note that the client returns a Gui while the server returns a Container <source lang=java> package tco.tiny;

import net.minecraft.entity.player.EntityPlayer; import net.minecraft.tileentity.TileEntity; import net.minecraft.world.World; import cpw.mods.fml.common.network.IGuiHandler;

public class GuiHandler implements IGuiHandler { //returns an instance of the Container you made earlier @Override public Object getServerGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { TileEntity tileEntity = world.getBlockTileEntity(x, y, z); if(tileEntity instanceof TileEntityTiny){ return new ContainerTiny(player.inventory, (TileEntityTiny) tileEntity); } return null; }

//returns an instance of the Gui you made earlier @Override public Object getClientGuiElement(int id, EntityPlayer player, World world, int x, int y, int z) { TileEntity tileEntity = world.getBlockTileEntity(x, y, z); if(tileEntity instanceof TileEntityTiny){ return new GuiTiny(player.inventory, (TileEntityTiny) tileEntity); } return null;

} } </source> Then, register the gui handler in your mod file. <source lang=java> //"this" is an instance of the @Mod NetworkRegistry.instance().registerGuiHandler(this, new GuiHandler()); </source> Now we just need to open the Gui. Heres the completed player.openGui: <source lang=java> player.openGui(Tiny.instance, 0, world, x, y, z); </source>

Syncing Progress Bars / Integer Data

  • Look at ContainerFurnace for info on doing this.*

Buttons

Sending button clicks to the server is more complicated. We need to send a packet with the necessary info and handle that packet in the PacketHandler. Here's an example of initGui and actionPerformed in the gui class. Note that a corresponding packet handler is required <source> @Override public void initGui() { super.initGui(); //make buttons //id, x, y, width, height, text buttonList.add(new GuiButton(1, 10, 52, 20, 20, "+")); buttonList.add(new GuiButton(2, 40, 72, 20, 20, "-")); }

protected void actionPerformed(GuiButton guibutton) { //id is the id you give your button switch(guibutton.id) { case 1: i += 1; break; case 2: i -= 1; } //Packet code here //PacketDispatcher.sendPacketToServer(packet); //send packet } </source>

Multiple Guis

You may have noticed the id parameter for the methods in IGuiHandler. That is your GUI Id, which is required for multiple guis within one mod. Simply add switch or if/else statements in the IGuiHandler methods to check the id and act accordingly. To open a gui with a specific id, use: <source lang=java> //id = your gui id player.openGui(YourMod.instance, id, world, x, y, z); </source>

Here's the my test @Mod file. (testing only) <source lang=java> package tco.tiny;

import net.minecraft.block.Block; import net.minecraft.item.ItemBlock; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.*; import cpw.mods.fml.common.event.FMLInitializationEvent; import cpw.mods.fml.common.network.NetworkMod; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.GameRegistry;

@Mod(modid="Tiny", name="Tiny", version="1.0") @NetworkMod(clientSideRequired=true) public class Tiny {

@Instance("Tiny") public static Tiny instance;

public static Block blockTiny;

@EventHandler public void init(FMLInitializationEvent event) { blockTiny = new BlockTiny(1337); GameRegistry.registerBlock(blockTiny, ItemBlock.class, "blockTiny"); GameRegistry.registerTileEntity(TileEntityTiny.class, "containerTiny"); NetworkRegistry.instance().registerGuiHandler(this, new GuiHandler()); }

} </source> {{#set:Difficulty=Intermediate}}