Minecraft Modding: Container Blocks (Blocks With Inventory)

Introduction


In Minecraft there are numerous blocks you can interact with, mostly to transform items -- cook something in a furnace, brew something on a brewing stand, etc.  These blocks all make use of a Container to act as a collection point for a number of Slots that can hold items, and importantly the Container makes it easy to combine the player inventory into the GUI for the block (e.g. so you can have the standard view of all the player inventory at the bottom of the GUI).

Note: You only need a container if your GUI needs to have slots with items.  If your GUI is simple, such as just information like a sign or maybe a machine that just has a button to turn it on, you do not need a container -- just call your custom GUI (which should extend GuiScreen, not GuiContainer) directly from the onBlockActivated() method in your custom block.

Key Point: Normally in modding I suggest simply copying vanilla things that are similar and modifying them. That won't quite work in this case because if you copy something like a BlockFurnace you'll find that your custom furnace acts screwy -- moving items around will sort of disappear or randomly move around slots.  This is because vanilla container blocks also make use of some client-server synchronization packets. To achieve similar synchronization in your custom container block Forge has provided the IGuiHandler interface.  To understand a bit about the communication between client and server for containers see TheGreyGhost's Container Explanation.

The Steps To Create A Working Container Block


This tutorial will run through the basics of getting a functional container block working.  The main steps are:
  1. Create a custom block class that extends BlockContainer.
  2. Instantiate that block in your mod's main class or a "Blocks" instance list class.
  3. Register the block same as any other custom block in your proxy.
  4. Register the block renderer.  In 1.8 you'll also need to ensure you have the blockstates JSON, model JSON, and block item JSON properly set up in you assets.  See Jabelar's Block Tutorial for more information on getting blocks set up.
  5. Create a class that implements IInventory:
    • If you need a tile entity (basically if your block will process like a furnace even when not in the GUI), then create a custom TileEntity that extends TileEntityLockable and implements IUpdatePlayerListBox and ISidedInventory. Register the tile entity like any other custom tile entity in your proxy.
    • Otherwise, just create a separate custom inventory class that extends IInventory.
  6. Create a custom container class that extends Container.
  7. Create any custom slot classes needed (if any) that extend Slot.
  8. Create a custom GUI class that extends GuiContainer.
  9. Create a custom GUI handler class that implements IGuiHandler.
  10. Register the GUI handler in your proxy.
  11. (Optional but recommended) Create a recipes class to organize possible actions your block can take.
  12. Update the lang file for the blocks ("tile") and containers (this is what displays in the GUI as the title).
Whew!  As you can see there is a lot required, so hopefully the following detailed explanation helps.

In this tutorial I'll create a "grinder" block that takes a single type of item as input and over time grinds it into something else -- like stone blocks into sand.

Create A Custom Block Class That Extends BlockContainer.


Here is an example:

public class BlockGrinder extends BlockContainer
{
    public static final PropertyDirection FACING = 
          PropertyDirection.create("facing", 
          EnumFacing.Plane.HORIZONTAL);
    private static boolean hasTileEntity;

    public BlockGrinder()
    {
        super(Material.rock);
        setUnlocalizedName("grinder");
        setDefaultState(blockState.getBaseState().withProperty(
              FACING, EnumFacing.NORTH));
        setCreativeTab(CreativeTabs.tabDecorations);
        stepSound = soundTypeSnow;
        blockParticleGravity = 1.0F;
        slipperiness = 0.6F;
        setBlockBounds(0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 1.0F);
        lightOpacity = 20; // cast a light shadow
        setTickRandomly(false);
        useNeighborBrightness = false;
    }
    @Override
    public Item getItemDropped(
          IBlockState state, 
          Random rand, 
          int fortune)
    {
        return Item.getItemFromBlock(BlockSmith.blockGrinder);
    }

    @Override
    public void onBlockAdded(
          World parWorld, 
          BlockPos parBlockPos, 
          IBlockState parIBlockState)
    {
        if (!parWorld.isRemote)
        {
            // Rotate block if the front side is blocked
            Block blockToNorth = parWorld.getBlockState(
                  parBlockPos.offsetNorth()).getBlock();
            Block blockToSouth = parWorld.getBlockState(
                  parBlockPos.offsetSouth()).getBlock();
            Block blockToWest = parWorld.getBlockState(
                  parBlockPos.offsetWest()).getBlock();
            Block blockToEast = parWorld.getBlockState(
                  parBlockPos.offsetEast()).getBlock();
            EnumFacing enumfacing = (EnumFacing)parIBlockState
                  .getValue(FACING);

            if (enumfacing == EnumFacing.NORTH 
                  && blockToNorth.isFullBlock() 
                  && !blockToSouth.isFullBlock())
            {
                enumfacing = EnumFacing.SOUTH;
            }
            else if (enumfacing == EnumFacing.SOUTH 
                  && blockToSouth.isFullBlock() 
                  && !blockToNorth.isFullBlock())
            {
                enumfacing = EnumFacing.NORTH;
            }
            else if (enumfacing == EnumFacing.WEST 
                  && blockToWest.isFullBlock() 
                  && !blockToEast.isFullBlock())
            {
                enumfacing = EnumFacing.EAST;
            }
            else if (enumfacing == EnumFacing.EAST 
                  && blockToEast.isFullBlock() 
                  && !blockToWest.isFullBlock())
            {
                enumfacing = EnumFacing.WEST;
            }

            parWorld.setBlockState(parBlockPos, parIBlockState
                  .withProperty(FACING, enumfacing), 2);
        }
    }

    @Override
    public boolean onBlockActivated(
          World parWorld, 
          BlockPos parBlockPos, 
          IBlockState parIBlockState, 
          EntityPlayer parPlayer, 
          EnumFacing parSide, 
          float hitX, 
          float hitY, 
          float hitZ)
    {
        if (!parWorld.isRemote)
        {
            parPlayer.openGui(BlockSmith.instance, 
                  BlockSmith.GUI_ENUM.GRINDER.ordinal(), 
                  parWorld, 
                  parBlockPos.getX(), 
                  parBlockPos.getY(), 
                  parBlockPos.getZ()); 
        }
        
        return true;
    }

    @Override
    public TileEntity createNewTileEntity(World worldIn, int meta)
    {
     // DEBUG
     System.out.println("BlockGrinder createNewTileEntity()");
        return new TileEntityGrinder();
    }

    @Override
    public IBlockState onBlockPlaced(
          World worldIn, 
          BlockPos pos, 
          EnumFacing facing, 
          float hitX, 
          float hitY, 
          float hitZ, 
          int meta, 
          EntityLivingBase placer)
    {
        return getDefaultState().withProperty(FACING, 
              placer.func_174811_aO().getOpposite());
    }

    @Override
    public void onBlockPlacedBy(
          World worldIn, 
          BlockPos pos, 
          IBlockState state, 
          EntityLivingBase placer, 
          ItemStack stack)
    {
        worldIn.setBlockState(pos, state.withProperty(
              FACING, 
              placer.func_174811_aO().getOpposite()), 
              2);
    }

    @Override
    public void breakBlock(
          World worldIn, 
          BlockPos pos, 
          IBlockState state)
    {
        if (!hasTileEntity)
        {
            TileEntity tileentity = worldIn.getTileEntity(pos);

            if (tileentity instanceof TileEntityGrinder)
            {
                InventoryHelper.dropInventoryItems(worldIn, pos, 
                      (TileEntityGrinder)tileentity);
                worldIn.updateComparatorOutputLevel(pos, this);
            }
        }

        super.breakBlock(worldIn, pos, state);
    }

    @Override
    @SideOnly(Side.CLIENT)
    public Item getItem(World worldIn, BlockPos pos)
    {
        return Item.getItemFromBlock(BlockSmith.blockGrinder);
    }

    @Override
    public int getRenderType()
    {
        return 3;
    }
    @Override
    @SideOnly(Side.CLIENT)
    public IBlockState getStateForEntityRender(IBlockState state)
    {
        return getDefaultState().withProperty(FACING, EnumFacing.SOUTH);
    }

    @Override
    public IBlockState getStateFromMeta(int meta)
    {
        EnumFacing enumfacing = EnumFacing.getFront(meta);

        if (enumfacing.getAxis() == EnumFacing.Axis.Y)
        {
            enumfacing = EnumFacing.NORTH;
        }

        return getDefaultState().withProperty(FACING, enumfacing);
    }

    @Override
    public int getMetaFromState(IBlockState state)
    {
        return ((EnumFacing)state.getValue(FACING)).getIndex();
    }

    @Override
    protected BlockState createBlockState()
    {
        return new BlockState(this, new IProperty[] {FACING});
    }

    @SideOnly(Side.CLIENT)
    static final class SwitchEnumFacing
    {
        static final int[] enumFacingArray = new int[EnumFacing.values()
              .length];

        static
        {
            try
            {
                enumFacingArray[EnumFacing.WEST.ordinal()] = 1;
            }
            catch (NoSuchFieldError var4)
            {
                ;
            }

            try
            {
                enumFacingArray[EnumFacing.EAST.ordinal()] = 2;
            }
            catch (NoSuchFieldError var3)
            {
                ;
            }

            try
            {
                enumFacingArray[EnumFacing.NORTH.ordinal()] = 3;
            }
            catch (NoSuchFieldError var2)
            {
                ;
            }

            try
            {
                enumFacingArray[EnumFacing.SOUTH.ordinal()] = 4;
            }
            catch (NoSuchFieldError var1)
            {
                // You should improve the error handling here
            }
        }
    }
}    

Hopefully the above code is mostly self explanatory.

My block happens to be "sided" meaning it has a front so I to rotate it when placed to face the player and also and also to ensure the front is accessible.  So in the onBlockAdded() method I am checking and rotating the block accordingly.

Key Point. The onBlockActivated() method is the one that should open your custom GUI.

Key Point: The createNewTileEntity() method is essential if your block is supposed to be associated wtih a tile entity.  It is called automatically when your block is placed, and should simply create your tile entity in the same position.

The onBlockPlaced() method is simple method to retrieve the block state for use in the onBlockPlacedAt() method. The onBlockPlacedAt() method is described by the comments in the source as "called to actually place the block, after the location is determined and all permission checks have been made." You can simply update the state property here.

Key Point: The breakBlock() method is important as this is where you can ensure you get your inventory items back -- e.g. if someone leaves items in the block and then breaks it they will lose those items unless you drop them.  So my code drops them (my custom tile entity implements an inventory, so I call the InventoryHelper.dropInventoryItems() method passing the tile entity as a parameter)..

Instantiate The Block In Mod's Main Class or "Blocks" Instance List Class


Somewhere you collect instances such as in your mod's main class or in a "Blocks" class, you need to create an instance of the block, such as like this:

public final static BlockGrinder blockGrinder = new BlockGrinder();


Register The Block


In the pre-init handler of your common proxy class, register the block with something like this:

GameRegistry.registerBlock(BlockSmith.blockGrinder, BlockSmith.blockGrinder.getUnlocalizedName().substring(5));

Register The Block Renderer


In the init handler of your client proxy class, register the block (and item block) renderer with something like:


RenderItem renderItem = Minecraft.getMinecraft().getRenderItem();
renderItem.getItemModelMesher().register(
      Item.getItemFromBlock(BlockSmith.blockGrinder), 
      0, 
      new ModelResourceLocation(
            BlockSmith.MODID + ":" + BlockSmith.blockGrinder
            .getUnlocalizedName().substring(5), 
            "inventory"));

Set Up Block JSON Assets


This is just regular block stuff. If you need help understanding this check out Jabelar's 1.8 Blocks Tutorial.

In this example, for the blockstates JSON I put:
{
    "variants": {
        "facing=north": { "model": "blocksmith:grinder" },
        "facing=south": { "model": "blocksmith:grinder", "y": 180 },
        "facing=west":  { "model": "blocksmith:grinder", "y": 270 },
        "facing=east":  { "model": "blocksmith:grinder", "y": 90 }
    }
}

For the block model JSON I put:
{
    "parent": "block/orientable",
    "textures": 
    {
        "top": "blocksmith:blocks/grinder_top",
        "front": "blocksmith:blocks/grinder_front",
        "side": "blocksmith:blocks/grinder_side"
    }
}

And for the item model JSON I put:
{
    "parent": "blocksmith:block/grinder",
    "display": {
        "thirdperson": {
            "rotation": [ 10, -45, 170 ],
            "translation": [ 0, 1.5, -2.75 ],
            "scale": [ 0.375, 0.375, 0.375 ]
        }
    }
}

Create A Class (Possibly A TileEntity) That Implements IInventory:


If you need a tile entity, then create a custom TileEntity that extends TileEntityLockable and implements IUpdatePlayerListBox and ISidedInventory. Register the tile entity like any other custom tile entity in your proxy. Otherwise, you could just create a separate custom inventory class that extends IInventory.
In this case I used a TileEntity since I want the grinding to happen over time:

public class TileEntityGrinder extends TileEntityLockable 
      implements IUpdatePlayerListBox, ISidedInventory
{
    // enumerate the slots
    public enum slotEnum 
    {
        INPUT_SLOT, OUTPUT_SLOT
    }
    private static final int[] slotsTop = new int[] {
          slotEnum.INPUT_SLOT.ordinal()};
    private static final int[] slotsBottom = new int[] {
          slotEnum.OUTPUT_SLOT.ordinal()};
    private static final int[] slotsSides = new int[] {};
    private ItemStack[] grinderItemStackArray = new ItemStack[2];
    private int timeCanGrind; 
    private int currentItemGrindTime;
    private int ticksGrindingItemSoFar;
    private int ticksPerItem;
    private String grinderCustomName;

    @Override
    public boolean shouldRefresh(World parWorld, BlockPos parPos, 
          IBlockState parOldState, IBlockState parNewState)
    {
        return false;
    }

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

    @Override
    public ItemStack getStackInSlot(int index)
    {
        return grinderItemStackArray[index];
    }

    @Override
    public ItemStack decrStackSize(int index, int count)
    {
        if (grinderItemStackArray[index] != null)
        {
            ItemStack itemstack;

            if (grinderItemStackArray[index].stackSize <= count)
            {
                itemstack = grinderItemStackArray[index];
                grinderItemStackArray[index] = null;
                return itemstack;
            }
            else
            {
                itemstack = grinderItemStackArray[index].splitStack(count);

                if (grinderItemStackArray[index].stackSize == 0)
                {
                    grinderItemStackArray[index] = null;
                }

                return itemstack;
            }
        }
        else
        {
            return null;
        }
    }

    /**
     * When some containers are closed they call this on each slot, then 
     * drop whatever it returns as an EntityItem -
     * like when you close a workbench GUI.
     */
    @Override
    public ItemStack getStackInSlotOnClosing(int index)
    {
        if (grinderItemStackArray[index] != null)
        {
            ItemStack itemstack = grinderItemStackArray[index];
            grinderItemStackArray[index] = null;
            return itemstack;
        }
        else
        {
            return null;
        }
    }

    @Override
    public void setInventorySlotContents(int index, ItemStack stack)
    {
        boolean isSameItemStackAlreadyInSlot = stack != null 
              && stack.isItemEqual(grinderItemStackArray[index]) 
              && ItemStack.areItemStackTagsEqual(stack, 
                    grinderItemStackArray[index]);
        grinderItemStackArray[index] = stack;

        if (stack != null && stack.stackSize > getInventoryStackLimit())
        {
            stack.stackSize = getInventoryStackLimit();
        }

        // if input slot, reset the grinding timers
        if (index == slotEnum.INPUT_SLOT.ordinal() 
              && !isSameItemStackAlreadyInSlot)
        {
            ticksPerItem = timeToGrindOneItem(stack);
            ticksGrindingItemSoFar = 0;
            markDirty();
        }
    }

    @Override
    public String getName()
    {
        return hasCustomName() ? grinderCustomName : "container.grinder";
    }

    @Override
    public boolean hasCustomName()
    {
        return grinderCustomName != null && grinderCustomName.length() > 0;
    }

    public void setCustomInventoryName(String parCustomName)
    {
        grinderCustomName = parCustomName;
    }

    @Override
    public void readFromNBT(NBTTagCompound compound)
    {
        super.readFromNBT(compound);
        NBTTagList nbttaglist = compound.getTagList("Items", 10);
        grinderItemStackArray = new ItemStack[getSizeInventory()];

        for (int i = 0; i < nbttaglist.tagCount(); ++i)
        {
            NBTTagCompound nbtTagCompound = nbttaglist.getCompoundTagAt(i);
            byte b0 = nbtTagCompound.getByte("Slot");

            if (b0 >= 0 && b0 < grinderItemStackArray.length)
            {
                grinderItemStackArray[b0] = ItemStack.loadItemStackFromNBT(
                      nbtTagCompound);
            }
        }

        timeCanGrind = compound.getShort("GrindTime");
        ticksGrindingItemSoFar = compound.getShort("CookTime");
        ticksPerItem = compound.getShort("CookTimeTotal");

        if (compound.hasKey("CustomName", 8))
        {
            grinderCustomName = compound.getString("CustomName");
        }
    }

    @Override
    public void writeToNBT(NBTTagCompound compound)
    {
        super.writeToNBT(compound);
        compound.setShort("GrindTime", (short)timeCanGrind);
        compound.setShort("CookTime", (short)ticksGrindingItemSoFar);
        compound.setShort("CookTimeTotal", (short)ticksPerItem);
        NBTTagList nbttaglist = new NBTTagList();

        for (int i = 0; i < grinderItemStackArray.length; ++i)
        {
            if (grinderItemStackArray[i] != null)
            {
                NBTTagCompound nbtTagCompound = new NBTTagCompound();
                nbtTagCompound.setByte("Slot", (byte)i);
                grinderItemStackArray[i].writeToNBT(nbtTagCompound);
                nbttaglist.appendTag(nbtTagCompound);
            }
        }

        compound.setTag("Items", nbttaglist);

        if (hasCustomName())
        {
            compound.setString("CustomName", grinderCustomName);
        }
    }

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

    public boolean grindingSomething()
    {
        return true;
    }

    // this function indicates whether container texture should be drawn
    @SideOnly(Side.CLIENT)
    public static boolean func_174903_a(IInventory parIInventory)
    {
        return true ;
    }

    @Override
    public void update()
    {
        boolean hasBeenGrinding = grindingSomething();
        boolean changedGrindingState = false;

        if (grindingSomething())
        {
            --timeCanGrind;
        }

        if (!worldObj.isRemote)
        {
            // if something in input slot
            if (grinderItemStackArray[slotEnum.INPUT_SLOT.ordinal()] != 
                  null)
            {                
                 // start grinding
                if (!grindingSomething() && canGrind())
                {
                     timeCanGrind = 150;
    
                     if (grindingSomething())
                     {
                         changedGrindingState = true;
                     }
                }

                // continue grinding
                if (grindingSomething() && canGrind())
                {
                    ++ticksGrindingItemSoFar;
                    
                    // check if completed grinding an item
                    if (ticksGrindingItemSoFar == ticksPerItem)
                    {
                        ticksGrindingItemSoFar = 0;
                        ticksPerItem = timeToGrindOneItem(
                              grinderItemStackArray[0]);
                        grindItem();
                        changedGrindingState = true;
                    }
                }
                else
                {
                    ticksGrindingItemSoFar = 0;
                }
            }

            // started or stopped grinding, update block to change to active 
            // or inactive model
            if (hasBeenGrinding != grindingSomething())
            {
                changedGrindingState = true;
                BlockGrinder.changeBlockBasedOnGrindingStatus(
                      grindingSomething(), worldObj, pos);
            }
        }

        if (changedGrindingState)
        {
            markDirty();
        }
    }

    public int timeToGrindOneItem(ItemStack parItemStack)
    {
        return 200;
    }

    private boolean canGrind()
    {
        // if nothing in input slot
        if (grinderItemStackArray[slotEnum.INPUT_SLOT.ordinal()] == null)
        {
            return false;
        }
        else // check if it has a grinding recipe
        {
            ItemStack itemStackToOutput = GrinderRecipes.instance()
                  .getGrindingResult(grinderItemStackArray[
                        slotEnum.INPUT_SLOT.ordinal()]);
            if (itemStackToOutput == null) return false; 
            if (grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()] 
                  == null) return true; 
            if (!grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()]
                 .isItemEqual(itemStackToOutput)) return false; 
            int result = grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()]
                  .stackSize + itemStackToOutput.stackSize;
            return result <= getInventoryStackLimit() 
                  && result <= grinderItemStackArray[slotEnum
                        .OUTPUT_SLOT.ordinal()].getMaxStackSize();
        }
    }

    public void grindItem()
    {
        if (canGrind())
        {
            ItemStack itemstack = GrinderRecipes.instance()
                  .getGrindingResult(grinderItemStackArray[
                        slotEnum.INPUT_SLOT.ordinal()]);

            // check if output slot is empty
            if (grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()] == null)
            {
                grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()] = 
                      itemstack.copy();
            }
            else if (grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()]
                  .getItem() == itemstack.getItem())
            {
                grinderItemStackArray[slotEnum.OUTPUT_SLOT.ordinal()]
                      .stackSize += itemstack.stackSize; 
            }

            --grinderItemStackArray[slotEnum.INPUT_SLOT.ordinal()].stackSize;

            if (grinderItemStackArray[slotEnum.INPUT_SLOT.ordinal()]
                  .stackSize <= 0)
            {
                grinderItemStackArray[slotEnum.INPUT_SLOT.ordinal()] = null;
            }
        }
    }

    @Override
    public boolean isUseableByPlayer(EntityPlayer playerIn)
    {
        return worldObj.getTileEntity(pos) != this ? false : 
              playerIn.getDistanceSq(pos.getX()+0.5D, pos.getY()+0.5D, 
              pos.getZ()+0.5D) <= 64.0D;
    }

    @Override
    public void openInventory(EntityPlayer playerIn) {}

    @Override
    public void closeInventory(EntityPlayer playerIn) {}

    @Override
    public boolean isItemValidForSlot(int index, ItemStack stack)
    {
        return index == slotEnum.INPUT_SLOT.ordinal() ? true : false; 
    }

    @Override
    public int[] getSlotsForFace(EnumFacing side)
    {
        return side == EnumFacing.DOWN ? slotsBottom : 
              (side == EnumFacing.UP ? slotsTop : slotsSides);
    }

    @Override
    public boolean canInsertItem(int index, ItemStack itemStackIn, 
          EnumFacing direction)
    {
        return isItemValidForSlot(index, itemStackIn);
    }

    @Override
    public boolean canExtractItem(int parSlotIndex, ItemStack parStack, 
          EnumFacing parFacing)
    {
        return true;
    }

    @Override
    public String getGuiID()
    {
        return "blocksmith:grinder";
    }

    @Override
    public Container createContainer(InventoryPlayer playerInventory, 
          EntityPlayer playerIn)
    {
        // DEBUG
        System.out.println("TileEntityGrinder createContainer()");
        return new ContainerGrinder(playerInventory, this);
    }

    @Override
    public int getField(int id)
    {
        switch (id)
        {
            case 0:
                return timeCanGrind;
            case 1:
                return currentItemGrindTime;
            case 2:
                return ticksGrindingItemSoFar;
            case 3:
                return ticksPerItem;
            default:
                return 0;
        }
    }

    @Override
    public void setField(int id, int value)
    {
        switch (id)
        {
            case 0:
                timeCanGrind = value;
                break;
            case 1:
                currentItemGrindTime = value;
                break;
            case 2:
                ticksGrindingItemSoFar = value;
                break;
            case 3:
                ticksPerItem = value;
                break;
        default:
            break;
        }
    }

    @Override
    public int getFieldCount()
    {
        return 4;
    }

    @Override
    public void clear()
    {
        for (int i = 0; i < grinderItemStackArray.length; ++i)
        {
            grinderItemStackArray[i] = null;
        }
    }
}

Register You TileEntity


Since I created a tile entity class, I need to make sure I register it. In the pre-init handling method in your common proxy, put something like this:

GameRegistry.registerTileEntity(TileEntityGrinder.class, "tileEntityGrinder");               

Create A Class That Extends Container.


For example, here is mine for the grinder:

public class ContainerGrinder extends Container
{
    private final IInventory tileGrinder;
    private final int sizeInventory;
    private int ticksGrindingItemSoFar;
    private int ticksPerItem;
    private int timeCanGrind;

    public ContainerGrinder(InventoryPlayer parInventoryPlayer, 
          IInventory parIInventory)
    {
        // DEBUG
        System.out.println("ContainerGrinder constructor()");
        
        tileGrinder = parIInventory;
        sizeInventory = tileGrinder.getSizeInventory();
        addSlotToContainer(new Slot(tileGrinder, 
              TileEntityGrinder.slotEnum.INPUT_SLOT.ordinal(), 56, 35));
        addSlotToContainer(new SlotGrinderOutput(parInventoryPlayer.player, 
              tileGrinder, TileEntityGrinder.slotEnum.OUTPUT_SLOT.ordinal(), 
              116, 35));
        
        // add player inventory slots
        // note that the slot numbers are within the player inventory so can 
        // be same as the tile entity inventory
        int i;
        for (i = 0; i < 3; ++i)
        {
            for (int j = 0; j < 9; ++j)
            {
                addSlotToContainer(new Slot(parInventoryPlayer, j+i*9+9, 
                8+j*18, 84+i*18));
            }
        }

        // add hotbar slots
        for (i = 0; i < 9; ++i)
        {
            addSlotToContainer(new Slot(parInventoryPlayer, i, 8 + i * 18, 
            142));
        }
    }

    @Override
    public void addCraftingToCrafters(ICrafting listener)
    {
        super.addCraftingToCrafters(listener);
        listener.func_175173_a(this, tileGrinder);
    }

    /**
     * Looks for changes made in the container, sends them to every listener.
     */
    @Override
    public void detectAndSendChanges()
    {
        super.detectAndSendChanges();

        for (int i = 0; i < crafters.size(); ++i)
        {
            ICrafting icrafting = (ICrafting)crafters.get(i);

            if (ticksGrindingItemSoFar != tileGrinder.getField(2))
            {
                icrafting.sendProgressBarUpdate(this, 2, 
                      tileGrinder.getField(2));
            }

            if (timeCanGrind != tileGrinder.getField(0))
            {
                icrafting.sendProgressBarUpdate(this, 0, 
                      tileGrinder.getField(0));
            }

            if (ticksPerItem != tileGrinder.getField(3))
            {
                icrafting.sendProgressBarUpdate(this, 3, 
                      tileGrinder.getField(3));
            }
        }

        ticksGrindingItemSoFar = tileGrinder.getField(2);
        timeCanGrind = tileGrinder.getField(0); 
        ticksPerItem = tileGrinder.getField(3); 
    }

    @Override
    @SideOnly(Side.CLIENT)
    public void updateProgressBar(int id, int data)
    {
        tileGrinder.setField(id, data);
    }

    @Override
    public boolean canInteractWith(EntityPlayer playerIn)
    {
        return tileGrinder.isUseableByPlayer(playerIn);
    }

    @Override
    public ItemStack transferStackInSlot(EntityPlayer playerIn, 
          int slotIndex)
    {
        ItemStack itemStack1 = null;
        Slot slot = (Slot)inventorySlots.get(slotIndex);

        if (slot != null && slot.getHasStack())
        {
            ItemStack itemStack2 = slot.getStack();
            itemStack1 = itemStack2.copy();

            if (slotIndex == TileEntityGrinder.slotEnum.OUTPUT_SLOT
                  .ordinal())
            {
                if (!mergeItemStack(itemStack2, sizeInventory, 
                      sizeInventory+36, true))
                {
                    return null;
                }

                slot.onSlotChange(itemStack2, itemStack1);
            }
            else if (slotIndex != TileEntityGrinder.slotEnum.INPUT_SLOT
                  .ordinal())
            {
                // check if there is a grinding recipe for the stack
                if (GrinderRecipes.instance().getGrindingResult(itemStack2) 
                      != null)
                {
                    if (!mergeItemStack(itemStack2, 0, 1, false))
                    {
                        return null;
                    }
                }
                else if (slotIndex >= sizeInventory 
                     && slotIndex < sizeInventory+27) // player inventory slots
                {
                    if (!mergeItemStack(itemStack2, sizeInventory+27, 
                          sizeInventory+36, false))
                    {
                        return null;
                    }
                }
                else if (slotIndex >= sizeInventory+27 
                      && slotIndex < sizeInventory+36 
                      && !mergeItemStack(itemStack2, sizeInventory+1, 
                      sizeInventory+27, false)) // hotbar slots
                {
                    return null;
                }
            }
            else if (!mergeItemStack(itemStack2, sizeInventory, 
                  sizeInventory+36, false))
            {
                return null;
            }

            if (itemStack2.stackSize == 0)
            {
                slot.putStack((ItemStack)null);
            }
            else
            {
                slot.onSlotChanged();
            }

            if (itemStack2.stackSize == itemStack1.stackSize)
            {
                return null;
            }

            slot.onPickupFromSlot(playerIn, itemStack2);
        }

        return itemStack1;
    }
}

In the constructor you need to associate your inventory class, in this case my tile entity.  Then you add the slots, for which I've created an enumeration for the slot type -- this can sometimes be useful if you want different slots to behave differently.

The addCraftingToCrafters() method is called by the EntityPlayerMP#openGui() method and it seems to be used to coorderinate between all the player clients. In this you want to add your inventory class, in this case the tile entity, to the list of crafter listeners.

The detectAndSentChanges() method takes the list of crafter listeners and notifies them of any fields that may have changed in the associated inventory class, in this case the tile entity.

The updateProgressBar() is just a really a method to set any of the custom fields in the inventory class, in this case the tile entity.  It doesn't have to be an actual progress bar, but for this grinder example I do use these to help fill in the progress arrow to show the progress of the grinding process.

The transferItemInSlot() method is important as it controls the behavior when clicking on slots and can generally be structured similar to what I show.

Create Any Custom Slot Classes Needed (If Any) that extend Slot.

Key Point: Custom slots are useful if you want to limit what can go into a slot. If you only want fuel to go in, for example.

In this example, I don't want the player to be able to put things into the output slot, so I create a custom slot like this:

public class SlotGrinderOutput  extends Slot
{
    /** The player that is using the GUI where this slot resides. */
    private final EntityPlayer thePlayer;
    private int numGrinderOutput;

    public SlotGrinderOutput(EntityPlayer parPlayer, 
          IInventory parIInventory, int parSlotIndex, 
          int parXDisplayPosition, int parYDisplayPosition)
    {
        super(parIInventory, parSlotIndex, parXDisplayPosition, 
              parYDisplayPosition);
        thePlayer = parPlayer;
    }

    @Override
    public boolean isItemValid(ItemStack stack)
    {
        return false; // can't place anything into it
    }

    @Override
    public ItemStack decrStackSize(int parAmount)
    {
        if (getHasStack())
        {
            numGrinderOutput += Math.min(parAmount, getStack().stackSize);
        }

        return super.decrStackSize(parAmount);
    }

    @Override
    public void onPickupFromSlot(EntityPlayer playerIn, ItemStack stack)
    {
        onCrafting(stack);
        super.onPickupFromSlot(playerIn, stack);
    }

    @Override
    protected void onCrafting(ItemStack parItemStack, int parAmountGround)
    {
        numGrinderOutput += parAmountGround;
        onCrafting(parItemStack);
    }

    @Override
    protected void onCrafting(ItemStack parItemStack)
    {
        if (!thePlayer.worldObj.isRemote)
        {
            int expEarned = numGrinderOutput;
            float expFactor = GrinderRecipes.instance()
                  .getGrindingExperience(parItemStack);

            if (expFactor == 0.0F)
            {
                expEarned = 0;
            }
            else if (expFactor < 1.0F)
            {
                int possibleExpEarned = MathHelper.floor_float(
                      expEarned*expFactor);

                if (possibleExpEarned < MathHelper.ceiling_float_int(
                      expEarned*expFactor) 
                      && Math.random() < expEarned*expFactor-possibleExpEarned)
                {
                    ++possibleExpEarned;
                }

                expEarned = possibleExpEarned;
            }

            // create experience orbs
            int expInOrb;
            while (expEarned > 0)
            {
                expInOrb = EntityXPOrb.getXPSplit(expEarned);
                expEarned -= expInOrb;
                thePlayer.worldObj.spawnEntityInWorld(
                      new EntityXPOrb(thePlayer.worldObj, thePlayer.posX, 
                            thePlayer.posY + 0.5D, thePlayer.posZ + 0.5D, 
                            expInOrb));
            }
        }

        numGrinderOutput = 0;

        // You can trigger achievements here based on output
        // E.g. if (parItemStack.getItem() == Items.grinded_fish)
        //      {
        //          thePlayer.triggerAchievement(AchievementList.grindFish);
        //      }
    }
}

Create a custom GUI class that extends GuiContainer.

Here is my example:


@SideOnly(Side.CLIENT)
public class GuiGrinder  extends GuiContainer
{
    private static final ResourceLocation grinderGuiTextures = 
         new ResourceLocation(BlockSmith.MODID
               +":textures/gui/container/grinder.png");
    private final InventoryPlayer inventoryPlayer;
    private final IInventory tileGrinder;

    public GuiGrinder(InventoryPlayer parInventoryPlayer, 
          IInventory parInventoryGrinder)
    {
        super(new ContainerGrinder(parInventoryPlayer, 
              parInventoryGrinder));
        inventoryPlayer = parInventoryPlayer;
        tileGrinder = parInventoryGrinder;
    }

    @Override
    protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY)
    {
        String s = tileGrinder.getDisplayName().getUnformattedText();
        fontRendererObj.drawString(s, xSize/2-fontRendererObj
              .getStringWidth(s)/2, 6, 4210752);
        fontRendererObj.drawString(inventoryPlayer.getDisplayName()
              .getUnformattedText(), 8, ySize - 96 + 2, 4210752);
    }

    /**
     * Args : renderPartialTicks, mouseX, mouseY
     */
    @Override
    protected void drawGuiContainerBackgroundLayer(float partialTicks, 
          int mouseX, int mouseY)
    {
        GlStateManager.color(1.0F, 1.0F, 1.0F, 1.0F);
        mc.getTextureManager().bindTexture(grinderGuiTextures);
        int marginHorizontal = (width - xSize) / 2;
        int marginVertical = (height - ySize) / 2;
        drawTexturedModalRect(marginHorizontal, marginVertical, 0, 0, 
              xSize, ySize);

        // Draw progress indicator
        int progressLevel = getProgressLevel(24);
        drawTexturedModalRect(marginHorizontal + 79, marginVertical + 34, 
              176, 14, progressLevel + 1, 16);
    }

    private int getProgressLevel(int progressIndicatorPixelWidth)
    {
        int ticksGrindingItemSoFar = tileGrinder.getField(2); 
        int ticksPerItem = tileGrinder.getField(3);
        return ticksPerItem != 0 && ticksGrindingItemSoFar != 0 ? 
              ticksGrindingItemSoFar*progressIndicatorPixelWidth/ticksPerItem 
              : 0;
    }
 }

Create a custom GUI handler class that implements IGuiHandler.


Tip: If your implementation has no need of a server element, then on the server-side you can return null for the getServerGuiElement() method's return value.  In no situation should you return null for the client-side.

Here is my example:

@SideOnly(Side.CLIENT)
public class GuiHandler implements IGuiHandler
{

    @Override
    public Object getServerGuiElement(int ID, EntityPlayer player, 
          World world, int x, int y, int z) 
    { 
        TileEntity tileEntity = world.getTileEntity(new BlockPos(x, y, z));

        if (tileEntity != null)
        {
            if (ID == BlockSmith.GUI_ENUM.GRINDER.ordinal())
            {
                return new ContainerGrinder(player.inventory, 
                      (IInventory)tileEntity);
            }
            if (ID == BlockSmith.GUI_ENUM.COMPACTOR.ordinal())
            {
                return new ContainerCompactor(player.inventory, 
                      (IInventory)tileEntity);
            }
        }
        if (ID == BlockSmith.GUI_ENUM.DECONSTRUCTOR.ordinal())
        {
            return new ContainerDeconstructor(player.inventory, world, 
                  x, y, z);
        }

        return null;
    }

    @Override
    public Object getClientGuiElement(int ID, EntityPlayer player, 
          World world, int x, int y, int z)
    {
        TileEntity tileEntity = world.getTileEntity(new BlockPos(x, y, z));

        if (tileEntity != null)
        {
            if (ID == BlockSmith.GUI_ENUM.GRINDER.ordinal())
            {
                return new GuiGrinder(player.inventory, 
                      (IInventory)tileEntity);
            }
            if (ID == BlockSmith.GUI_ENUM.COMPACTOR.ordinal())
            {
                return new GuiCompactor(player.inventory, 
                      (IInventory)tileEntity);
            }
        }
        if (ID == BlockSmith.GUI_ENUM.DECONSTRUCTOR.ordinal())
        {
            return new GuiDeconstructor(player.inventory, world, 
                  I18n.format("tile.deconstructor.name"), x, y, z);
        }
        return null;
    }
}

In this example, I'm showing how if you had multiple GUIs they all get handled through this same IGuiHandler class -- you check the ID and then return the appropriate server or client element.

To make the IDs a bit more understandable (you can just put 0, 1, 2, but it can be hard to find errors later) I have used an enum. But you can just use int values if you wish. Here is what my enum declaration looks like (in my mod's main class):

public enum GUI_ENUM 
{
    GRINDER, COMPACTOR, DECONSTRUCTOR, TANNING_RACK, FORGE
}

You'll notice that I'm also handling a GUI where there is not tile entity.

Register The GUI Handler In Your Common Proxy.


The GUI handler is related to networking and needs to be registered as such. In the init handling method of your common proxy, put something like this:


NetworkRegistry.INSTANCE.registerGuiHandler(BlockSmith.instance, 
      new GuiHandler());  

Warning: You should be doing this for every mod you make already but the BlockSmith.instance refers to an annotation you should have in your main class that creates an instance of the mod itself (which is needed in certain methods). So in my main mod class I have the following to initialize the instance:


@Instance(MODID)
public static BlockSmith instance;

(Optional But Recommended) Create A Recipes Class.


Theoretically you could manage the recipes by just checking the input value and using if-statements to determine the output. However, it is more organized to create a class to hold the possible transformation.

Here is my example:

public class GrinderRecipes 
{
    private static final GrinderRecipes grindingBase = new GrinderRecipes();
    /** The list of grinding results. */
    private final Map grindingList = Maps.newHashMap();
    private final Map experienceList = Maps.newHashMap();

    public static GrinderRecipes instance()
    {
        return grindingBase;
    }

    private GrinderRecipes()
    {
        addGrindingRecipe(
              new ItemStack(Item.getItemFromBlock(Blocks.stonebrick)), 
              new ItemStack(Item.getItemFromBlock(Blocks.gravel)), 0.7F);
        addGrindingRecipe(
              new ItemStack(Item.getItemFromBlock(Blocks.stone_slab)), 
              new ItemStack(Item.getItemFromBlock(Blocks.gravel)), 0.7F);
        addGrindingRecipe(
              new ItemStack(Item.getItemFromBlock(Blocks.stone_slab2)), 
              new ItemStack(Item.getItemFromBlock(Blocks.gravel)), 0.7F);
        addGrindingRecipe(
              new ItemStack(Item.getItemFromBlock(Blocks.stone_stairs)), 
              new ItemStack(Item.getItemFromBlock(Blocks.gravel)), 0.7F);
    }

    public void addGrindingRecipe(ItemStack parItemStackIn, 
          ItemStack parItemStackOut, float parExperience)
    {
        grindingList.put(parItemStackIn, parItemStackOut);
        experienceList.put(parItemStackOut, Float.valueOf(parExperience));
    }

    /**
     * Returns the grinding result of an item.
     */
    public ItemStack getGrindingResult(ItemStack parItemStack)
    {
        Iterator iterator = grindingList.entrySet().iterator();
        Entry entry;

        do
        {
            if (!iterator.hasNext())
            {
                return null;
            }

            entry = (Entry)iterator.next();
        }
        while (!areItemStacksEqual(parItemStack, (ItemStack)entry
              .getKey()));

        return (ItemStack)entry.getValue();
    }

    private boolean areItemStacksEqual(ItemStack parItemStack1, 
          ItemStack parItemStack2)
    {
        return parItemStack2.getItem() == parItemStack1.getItem() 
              && (parItemStack2.getMetadata() == 32767 
              || parItemStack2.getMetadata() == parItemStack1
              .getMetadata());
    }

    public Map getGrindingList()
    {
        return grindingList;
    }

    public float getGrindingExperience(ItemStack parItemStack)
    {
        Iterator iterator = experienceList.entrySet().iterator();
        Entry entry;

        do
        {
            if (!iterator.hasNext())
            {
                return 0.0F;
            }

            entry = (Entry)iterator.next();
        }
        while (!areItemStacksEqual(parItemStack, 
              (ItemStack)entry.getKey()));

        return ((Float)entry.getValue()).floatValue();
    }
}

Update .lang File


If you want to display text in your GUI, it is best to "localize it" meaning allow translation into other languages using the .lang file.

You can do this for the text of buttons, the title of the GUI, or whatever. In my example, I just have text for title of the GUI (i.e. so it will say "Grinder" at the top). So I put this in my .lang file.

container.grinder=Grinder

Note that I reference this in the drawScreen() method of my GUI.

Conclusion


I hope that this tutorial helps you through the creation of a container block, which is really rather complex.

As always, feel free to comment to ask for clarifications or suggest corrections.  Happy Modding!

50 comments:

  1. As I've been studying your website, and getting your help from the Minecraft forums, I feel like I need to ask about this one, even risking the fact it may make me sound dumb.

    While writing out your code, to see how it works my eclipse seems to have trouble with the .offsetNorth and related names, showing an object cast needs to be added to them.

    the only solution I can think of to your tutorial, is you've predefined these in your BlockSmith class, am I incorrect in that guess or have I over looked something?

    Tech

    ReplyDelete
    Replies
    1. Hi, thanks for writing.

      First of all, the code above is for 1.8. Is that what you're using?

      Secondly, for 1.8 the method names changed in the SRG mappings, so you need to make sure you have the latest SRG mappings. For some reason, if you just download the Forge it uses an old mapping from like December which is now way out of date. So in your build.gradle file in the Minecraft section you need to add something like:
      mappings = "snapshot_20150421"

      Note that the snapshot name is followed by a date, so you can change that to be the day before the day you're building the workspace and it will pick up latest snapshot. After adding that to the build.gradle file you need to run gradlew setupDecompWorkspace and gradlew eclipse again.

      Let me know if you still have trouble after checking whether the above explains your problem.

      Delete
    2. Hey Julian, thank you for replying, and helping out, I am using 1.8 for my environment, and I didn't know about the new mappings, I thought forge just set up everything when the latest version was installed, I'll update all of that after while and see if that helps. But for the time being it doesn't seem to be giving me an issue. any more.

      I've been following Minecraft's Furnace code, and Tileentity code, as the furnace I wish to make needs to have three slots two for ores, and one for output since its always on when it detects an ore in it, but that's later on. I have been referencing your code, and cross referencing to minecraft code, and my own, and I'm seeing some things that I don't completely understand yet, but I'm getting there. I'll update to the new SRG mappings when I get back to my project and let you know how things go.

      I've already created the furnace Block itself, I'm working on the Tileentity right now, and once I'm done with that, I'll be doing the gui, slots, and recipes.

      Thanks again

      Tech.

      Delete
    3. I just wanted to point out a way that I found that fixes the problem that Zion was having if updating the work space like Julian said is not working. All you have to do is change .offsetNorth to .north, this fixes the errors and seems to have no negative effects.

      If anybody would like to verify this method please feel free as I realize I am far from being a pro at modding.

      Delete
  2. Soon after I posted my previous comment I received an error that I can't seem to fix. The error is under the part that says; *func_174811_aO* I have no idea what function is supposed to take the place of this one or if it is defined later on but any help as to how to fix this would be great, thanks in advance.

    ReplyDelete
    Replies
    1. func_174811_aO changed to getHorizontalFacing as dictated in the BlockFurnace.java code. Most errors can be corrected by looking at the furnace's code which is practically the same

      Delete
  3. Could you please post your proxy code because I want to make sure that my proxy is set up properly. Also your main code would be very appreciated

    ReplyDelete
  4. How would I add more slots for fuel and a secondary item to be consumed along with the first one?

    ReplyDelete
  5. Hi, I'm having some trouble with this. I got everything working, almost. The inventory shows up fine, but I can't move anything, and it doesn't actually do anything. I was able to put an item in it once, and it did nothing, and yes, it was a valid recipe. Now, I can't move any items in the inventory. They immediately go back into the slot they were in when I try to pick them up. Did I mess something up? Thanks for any help. -727021

    ReplyDelete
    Replies
    1. Sorry for the delay in replying, I was traveling on holiday. Anyway, there may be a couple of things you need to look at. The first is that the slot contents are managed by the IGuiHandler and you need to make sure that that is registered and working properly. After that, you should check that the transferStackInSlot() method is working properly. To check that code is working properly, I suggest adding console log statements (using System.out.println() method) in various locations in the code so you can confirm that each expected method is being returned, and each expected condition (like for if statements) is met. If the two suggestions above don't help, keep tracing the code until you can follow it. Usually that will show the error -- you'll find a method isn't being called as expected, or an if statement isn't returning expected value -- and you can solve easily.

      Delete
  6. Hi, I have been following your gui creation tutorial and have ran into an error where when minecrfaft tries to create the guihandler it crashes with a nullpointer exception error. It would be great if you could lend me a hand. Thanks.

    ReplyDelete
    Replies
    1. Well, a null pointer exception is when you have code that tells Java to do something with something that doesn't "exist" yet -- no instance has been constructed/initialized. The Java keyword "new" is used to construct new instances, so you need to look at what is in the line of code that has the error and figure out why it doesn't exist at the time that line executes.

      Delete
  7. Hello, this tutorial is nice, however im using 1.7.10... Do you know of any tutorial working for 1.7.10?

    ReplyDelete
    Replies
    1. All the container, slot, inventory and gui stuff should be pretty much exactly the same in 1.7.10. Only the blocks have changed a bit and there are other tutorials that explain the differences. Also, you can compare my tutorial code with the vanilla code for furnace, chest, etc in 1.7.10 to get ideas on how to handle the differences.

      Delete
  8. Whenever I write while (!areItemStacksEqual(parItemStack, (ItemStack)entry.getKey()));, the getKey function says "The method getKey() is undefined for the type ResourcePackRepository.Entry" What have I done wrong?

    ReplyDelete
  9. And, it says "The method getValue() is undefined for the type ResourcePackRepository.Entry" for the line below it. (line 75 for previous comment and 77 for this one)

    ReplyDelete
    Replies
    1. Your errors are just Java errors. Your compiler thinks you have a field called "entry" that is of type ResourcePackRepository.Entry and are trying to get the key from that. However, that entry is supposed to be on the experienceList which is supposed to be type HashMap.

      So something in your code is making it think it is a ResourcePackRepository instead of a HashMap. Are you doing something else in your mod with resource pack repositories? Because I have absolutely no code related to that in this example... I think maybe you've somehow got the experienceList to be declared as a ResourcePackRepository somewhere.

      However, now that I look at my code, I could probably make the iteration a bit simpler. So maybe I'll rewrite it over the next couple days and maybe that might help.

      If you have your code on github or pastbin I could look at it as well to see if I see what the mistake is.

      Delete
    2. I just posted it on Patebin: http://pastebin.com/M35uqM7t remember line 75, 77, 114, 116 have errors

      Delete
  10. This comment has been removed by the author.

    ReplyDelete
    Replies
    1. Hi, your IDE will complain if the imports are not correct. I normally don't post the imports in my examples because they are a lot of text that makes the tutorial look messy. But it is important to make sure that all the imports are correct. It is possible to have two different things with same name (like Entry) from different Java packages. If you import the wrong one, you'll get complaints.

      Delete
    2. Here are the imports I used:

      import java.util.Iterator;
      import java.util.Map;
      import java.util.Map.Entry;

      import net.minecraft.init.Blocks;
      import net.minecraft.init.Items;
      import net.minecraft.item.Item;
      import net.minecraft.item.ItemStack;

      import com.google.common.collect.Maps;

      Delete
    3. OMG IT WORKED!!!!!!!!!! I had :
      import java.util.Iterator;
      import java.util.Map;

      import com.google.common.collect.Maps;

      import net.minecraft.client.resources.ResourcePackRepository.Entry;
      import net.minecraft.init.Blocks;
      import net.minecraft.item.Item;
      import net.minecraft.item.ItemStack;

      Delete
    4. Oh god, now it crashed. Here is the pastebin for the error log. It is a Null Pointer.
      http://pastebin.com/v40NVQj5

      Delete
    5. My mod FIle: http://pastebin.com/6evGuQdT

      Delete
    6. The code is the same as yours,mbut I only changed BlockSmith to BlockManager, my manager that registers and initializes blocks for rendering. I think it is the instance thing, i have never used it before

      Delete
    7. I didn't put it in this tutorial because you should already do this for any mod, but you need to create an annotated instance of the mod itself. In your code you just created an uninitialized Object instance. But instead you should have something like this:

      // instantiate the mod
      @Instance(MODID)
      public static ExampleMod.instance;

      Delete
    8. Correction the code should be:

      @Instance(MODID)
      public static BlockSmith instance;

      Delete
    9. NO WAY!!!! IT WORKS!! Thanks! Now, all I need ro find out is why the GUI us the purple and black texture, like the missing texture.

      Delete
    10. This comment has been removed by the author.

      Delete
    11. Well, it fuly works. Thanks, and I am done for now.

      Delete
  11. This comment has been removed by the author.

    ReplyDelete
  12. Thanks very very much for the blog post!! This has been tremendously helpful, and I know how much work this is.

    One question. Why do you override shouldRefresh(World, BlockPos, iBlockState, IBlockState) in your tile entity and have it always return false? While working through a defect in my mod where the client-side TE wasn't being removed I traced the call stack into this. I did find your other post about retain TE on block state change (1.8) and it seems to contradict what you do here about always returning false. Can you explain more about what is going on? Is there some way in the grinder that you get it to synchronize the TE state on the client?

    Thanks!
    -Andrew

    ReplyDelete
  13. Hey!
    This was very helpfull!
    Just one thing,
    I have a string in my GUI and i want to change it everytime the update() method on the tile entity class is called.
    How would I do that?
    TNX,
    Gal

    ReplyDelete
    Replies
    1. In the drawGuiForegroundLayer() method I think you would just look up whatever field you want from the associated tile entity. And in the tile entity you would update that field with the update() method.

      Really, if you want it to change over time, it is more like a furnace (which shows the progress). I have tutorial for that here: http://jabelarminecraft.blogspot.com/p/minecraft-modding-containers.html

      Delete
  14. Hi I have an error in my TileEntity class, it is: changeBlockBasedOnCompressingStatus() is undefined for the type BlockComrpessor. I have noticed that I really don' t have that function, but it also isn' t in the tutorial?

    please help

    ReplyDelete
    Replies
    1. I fixed it, but when my machine is active and I reload the world, all the items in the machine are gone, how can I fix this?

      Delete
  15. I believe the saving should be taken care of by the writeToNBT() and readFromNBT() methods in the TileEntity class. Did you implement those properly?

    ReplyDelete
    Replies
    1. i fixed it, i spelled items instead of Items. But I have a question: how do I make it so that my machine takes fuel, like a furnace?

      Delete
    2. Well, you would create an additional slot in the container and you would check what is in it. If there is something that matches the allowed fuel item, then start a counter for how long it can burn. When finishes burning, remove one from the item stack in the fuel slot. If there is nothing left, stop processing.

      All that is done in the TileEntity.

      Delete
    3. Thanks you, but how do I add a extra slot? when I try that it just copies the input slot

      Delete
    4. Would you please make an example? I can' t figure out how to do it

      Delete
  16. This comment has been removed by the author.

    ReplyDelete
  17. How would I go about adding a fuel slot to this code?

    ReplyDelete
  18. I tried to create my own custom inventory following your code but drag and drop events are not possible.

    ReplyDelete
    Replies
    1. Do I have to add something additional to make that possible?

      Delete
  19. This comment has been removed by the author.

    ReplyDelete
  20. I'm really hoping some day you'll update this tutorial to cover 1.10.2 or 1.11 versions of Minecraft. I find advanced tutorials for those newer versions are seriously lacking on the internet. Even better would be a tutorial showing how to adjust the 1.8 code (shown here) so it's valid for 1.10.2 or 1.11

    ReplyDelete
  21. Are these Codes for the Minecraft Version 1.8?

    ReplyDelete
  22. Hey,
    Your Tutorial is very helpful, but there that didn´t work. When I right-click my Grinder there is no Gui which comes up ;( can you help me asap?

    ReplyDelete
  23. topic is very nice.
    if you want to play game online casino please click this post.thank you.
    ดจีคลับ casino
    gclub online
    บา คา ร่า มือ ถือ

    ReplyDelete