Minecraft Modding: Block With GUI That Processes Instantly (De-Crafting Example)

Introduction


Sometimes you want to create a "machine" that processes input items over time like a furnace. If that is what you want, see Jabelar's Block With GUI That Processes Over Time Tutorial.) Otherwise you may want to make a "machine" that processes immediately like a crafting table. This tutorial explains how to do that by showing you how to make a "deconstructer" (like a reverse crafting table) machine.

Note: This tutorial was done with 1.8 code, but it should be easy to adapt to 1.7.10 since the GUI and Container stuff hasn't changed much.

Tip: Some people get confused on when to use a Container and TileEntity for their block. I explain the considerations here: Jabelar's Discussion On Blocks With GUIs.

In this case, we do not need a TileEntity.

The steps required to create the machine are:

  1. Create an enumeration to track different GUI IDs.
  2. Create a custom Block class for the machine.
  3. Create and properly file your block model and texture assets.
  4. Instantiate your block.
  5. Register the block.
  6. Register the block renderer.
  7. Create custom IInventory classes if needed.
  8. Create a custom Container class.
  9. Create a custom GuiContainer class.
  10. Create and properly file your GUI image asset.
  11. Create a custom IGuiHandler class.
  12. Register the IGuiHandler class.
  13. Create recipe management system.

Explanation Of What We're Making


The idea of this tutorial's machine is that it is essentially a "reverse crafting table". You can put an item in, and if there is a crafting recipe to make the item then it will put all the ingredients into an output matrix and will delete the input item.

Additionally, the GUI can display some information about errors -- like if there is no crafting recipe for the item.

Okay, now let's get going...

Create An Enumeration For Your GUI Types


It is likely that you will create multiple GUIs in a mod. And the openGUI() method requires a "GUI ID" to reference. Instead of using "magic numbers" good programming practice is to use a Java enumeration for the GUIs. I do this in my main class of my mod with code like this.

// enumerate guis
public enum GUI_ENUM 
{
    GRINDER, COMPACTOR, DECONSTRUCTOR, TANNING_RACK, FORGE
}

In this example, you can see I have several different GUIs in my mod, one of which is the DECONSTRUCTOR that we're making in this tutorial.

Create A Block Class For The Machine


Most people want their machine to be directional, meaning it has a front side. This isn't necessary if you don't want to, but you'll see that much of my block code is related to processing the facing direction.

public class BlockDeconstructor extends Block
{
    public static final PropertyDirection FACING = PropertyDirection.create(
          "facing", EnumFacing.Plane.HORIZONTAL);

    public BlockDeconstructor()
    {
        super(Material.rock);
        setUnlocalizedName("deconstructor");
        setCreativeTab(CreativeTabs.tabDecorations);
    }

    @Override
    public boolean onBlockActivated(
          World parWorld, 
          BlockPos parBlockPos, 
          IBlockState parIBlockState, 
          EntityPlayer parPlayer, 
          EnumFacing parSide, 
          float hitX, 
          float hitY, 
          float hitZ)
    {
        if(!parWorld.isRemote)
        {
         // DEBUG
         System.out.println("BlockDeconstructor onBlockActivated");
         
         parPlayer.openGui(
               BlockSmith.instance, 
               BlockSmith.GUI_ENUM.DECONSTRUCTOR.ordinal(), 
               parWorld, 
               parBlockPos.getX(), 
               parBlockPos.getY(), 
               parBlockPos.getZ()
               );
        }
        return true;
    }
    
    @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.north()).getBlock();
            Block blockToSouth = parWorld.getBlockState(parBlockPos.south()).getBlock();
            Block blockToWest = parWorld.getBlockState(parBlockPos.west()).getBlock();
            Block blockToEast = parWorld.getBlockState(parBlockPos.east()).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);
        }
    }

    /**
     * Possibly modify the given BlockState before rendering it on an Entity 
     * (Minecarts, Endermen, ...)
     */
    @Override
    @SideOnly(Side.CLIENT)
    public IBlockState getStateForEntityRender(IBlockState state)
    {
        return getDefaultState().withProperty(FACING, EnumFacing.SOUTH);
    }

    /**
     * Convert the given metadata into a BlockState for this Block
     */
    @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);
    }

    /**
     * Convert the BlockState into the correct metadata value
     */
    @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)
            {
                ;
            }
        }
    }
}

Hopefully most of this is self-explanatory. Everything related to the block states and EnumFacing are purely to allow the machine to have a front side.

The key method to look at though is the onBlockActivated() method. That is the one that opens the GUI. You'll want to change "BlockSmith" to the name of your mod's main class, and you'll want to reference your GUI class enumerator (which I explained how to create previously).

Tip: To get the "number" value of an enumerated type, you use the ordinal() method.

Create Your Block Model JSON And Texture PNG Assets


I will assume that you know how to make a block. In 1.8, you need to have JSON file that maps the block states to models, a JSON file for the block model that maps the textures to the state, and a JSON file for the item block that references the block model. Make sure these files are all correct and stored in the correct asset directory.

Here is my block state JSON:

{
    "variants": {
        "facing=north": { "model": "blocksmith:deconstructor" },
        "facing=south": { "model": "blocksmith:deconstructor", "y": 180 },
        "facing=west":  { "model": "blocksmith:deconstructor", "y": 270 },
        "facing=east":  { "model": "blocksmith:deconstructor", "y": 90 }
    }
}

Here is my block model JSON:

{
    "parent": "block/orientable",
    "textures": 
    {
        "top": "blocksmith:blocks/deconstructor_top",
        "front": "blocksmith:blocks/deconstructor_front",
        "side": "blocksmith:blocks/deconstructor_side"
    }
}

Here is my item block model JSON:

{
    "parent": "blocksmith:block/deconstructor",
    "display": {
        "thirdperson": {
            "rotation": [ 10, -45, 170 ],
            "translation": [ 0, 1.5, -2.75 ],
            "scale": [ 0.375, 0.375, 0.375 ]
        }
    }
}

Instantiate Your Block


Again, I will assume that you know how to make a block. Just a reminder that you need to create an instance of your block -- some people do this in their main class, some people in a class that contains all the block instances. Anyway, you'll want some code like this in one of those places:

public final static BlockDeconstructor blockDeconstructor = new BlockDeconstructor();

Register Your Block


And then in your common proxy class, in the method that handles the pre-init phase of loading, you need to register the block with something like this (replace "BlockSmith" with name of your main class):

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

Note: The reason I use the substring(5) on the getUnlocalizedName() result is that annoyingly Minecraft appends ".name" to the name. I find this annoying because it is not symmetric with the setUnlocalizeName() method -- a getter should return the same value as was set without alteration...

Register Your Block Renderer


Then in your client proxy class, in the method that handles the init phase of loading, you need to register the block and item block renderer like this:

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

Create Custom IInventory Classes If Needed


For most machines you'll want three inventories: input, output, and the player inventory. These will all be added by the Container.

The player inventory already exists. So we just need to make the input and output inventories. In our case, the InventoryCrafting class is suitable for the input inventory, but we want to create a custom IInventory class for the output results.

public class InventoryDeconstructResult implements IInventory
{
    private final ItemStack[] stackResult = new ItemStack[9];

    /**
     * Returns the number of slots in the inventory.
     */
    @Override
    public int getSizeInventory()
    {
        return 9;
    }

    /**
     * Returns the stack in slot i
     */
    @Override
    public ItemStack getStackInSlot(int par1)
    {
        return stackResult[par1];
    }

    /**
     * Removes from an inventory slot (first arg) up to a specified number
     * (second arg) of items and returns them in a new stack.
     */
    @Override
    public ItemStack decrStackSize(int par1, int par2)
    {
        if(stackResult[par1] != null)
        {
            ItemStack itemstack = stackResult[par1];
            stackResult[par1] = 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 par1)
    {
        if(stackResult[par1] != null)
        {
            ItemStack itemstack = stackResult[par1];
            stackResult[par1] = null;
            return itemstack;
        }
        else
        {
            return null;
        }
    }

    /**
     * Sets the given item stack to the specified slot in the inventory (can be
     * crafting or armor sections).
     */
    @Override
    public void setInventorySlotContents(int par1, ItemStack par2ItemStack)   
    {
        stackResult[par1] = par2ItemStack;
    }

    /**
     * Returns the maximum stack size for a inventory slot. Seems to always be
     * 64, possibly will be extended. *Isn't this more of a set than a get?*
     */
    @Override
    public int getInventoryStackLimit()
    {
        return 1;
    }

    @Override
    public boolean isUseableByPlayer(EntityPlayer par1EntityPlayer)
    {
        return true;
    }

    @Override
    public boolean isItemValidForSlot(int par1, ItemStack par2ItemStack)
    {
        return true;
    }

    public boolean isEmpty()
    {
        for(int i = 0; i < stackResult.length; i++ )
        {
            if(stackResult[i] != null)
                return false;
        }
        return true;
    }

    @Override
    public void markDirty()
    {

    }

    @Override
    public String getCommandSenderName()
    {
        return "DeconstructResult";
    }

    @Override
    public boolean hasCustomName()
    {
        return false;
    }

    @Override
    public IChatComponent getDisplayName()
    {
        return null;
    }

    @Override
    public void openInventory(EntityPlayer playerIn)
    {

    }

    @Override
    public void closeInventory(EntityPlayer playerIn)
    {

    }

    @Override
    public int getField(int id)
    {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void setField(int id, int value)
    {
        // TODO Auto-generated method stub

    }

    @Override
    public int getFieldCount()
    {
        // TODO Auto-generated method stub
        return 0;
    }

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

I actually didn't do much with this, except to set the size at 9 -- since we need a 3 x 3 grid for the reverse crafting results. The other methods are just copied from the InventoryCrafting class, but it is possible you could do more interesting things with these methods if needed for your machine.

Create A Container Class


A Container is a key part of any GUI that can accept items into or out of slots. The Container helps sync up the server and client so you don't need to make any of your own custom packets. It also helps combine multiple inventories (i.e. the inventory of the block and the inventory of the player).

So create a Container with something like this:

public class ContainerDeconstructor extends Container
{

    public static enum State
    {
        ERROR, READY
    }

    public InventoryCrafting inputInventory = new InventoryCrafting(this, 1, 1);
    public int inputSlotNumber;
    public InventoryDeconstructResult outputInventory = new InventoryDeconstructResult();
    public DeconstructingRecipeHandler deconstructingRecipeHandler;
    private final World worldObj;
    public InventoryPlayer playerInventory;
    public String resultString = "deconstructing.result.ready";
    public State deconstructingState = State.READY;
    public int x = 0;
    public int y = 0;
    public int z = 0;

    public ContainerDeconstructor(
          InventoryPlayer parPlayerInventory, 
          World parWorld, 
          int parX, 
          int parY, 
          int parZ
          )
    {
        x = parX;
        y = parY;
        z = parZ;
        worldObj = parWorld;
        
        deconstructingRecipeHandler = new DeconstructingRecipeHandler();
        
        for(int outputSlotIndexX = 0; outputSlotIndexX < 3; ++outputSlotIndexX)
        {
            for(int outputSlotIndexY = 0; outputSlotIndexY < 3; ++outputSlotIndexY)
            {
                addSlotToContainer(new Slot(outputInventory, outputSlotIndexY + 
                      outputSlotIndexX * 3, 112 + outputSlotIndexY * 18, 
                      17 + outputSlotIndexX * 18));
            }
        }
        
        inputSlotNumber = addSlotToContainer(new Slot(inputInventory, 0, 30 + 15, 35))
              .slotNumber;

        for(int playerSlotIndexY = 0; playerSlotIndexY < 3; ++playerSlotIndexY)
        {
            for(int playerSlotIndexX = 0; playerSlotIndexX < 9; ++playerSlotIndexX)
            {
                addSlotToContainer(new Slot(parPlayerInventory, playerSlotIndexX + 
                      playerSlotIndexY * 9 + 9, 8 + playerSlotIndexX * 18, 
                      84 + playerSlotIndexY * 18));
            }
        }
        
        for(int hotbarSlotIndex = 0; hotbarSlotIndex < 9; ++hotbarSlotIndex)
        {
            addSlotToContainer(new Slot(parPlayerInventory, hotbarSlotIndex, 
                  8 + hotbarSlotIndex * 18, 142));
        }     
        
        playerInventory = parPlayerInventory;
    }

    @Override
    public void onCraftMatrixChanged(IInventory parInventory)
    {
        if(parInventory == inputInventory)
        {
            if(inputInventory.getStackInSlot(0) == null)
            {
                resultString = I18n.format("deconstructing.result.ready");
                deconstructingState = State.READY;
                return;
            }
            int amountRequired = DeconstructingInputQuantity.getStackSizeNeeded(
                  inputInventory.getStackInSlot(0));
            // DEBUG
            System.out.println("Amount required = "+amountRequired);

            if(amountRequired > inputInventory.getStackInSlot(0).stackSize)
            {
                resultString = I18n.format("deconstructing.result.needMoreStacks", 
                      (amountRequired - inputInventory.getStackInSlot(0).stackSize));
                deconstructingState = State.ERROR;
                return;
            }
            
            if (amountRequired <= 0)
            {
                resultString = I18n.format("deconstructing.result.impossible");
                deconstructingState = State.ERROR;
                return;
            }
            
            ItemStack[] outputItemStackArray = deconstructingRecipeHandler
                  .getDeconstructResults(inputInventory.getStackInSlot(0));

            if (outputItemStackArray == null)
            {
                resultString = I18n.format("deconstructing.result.impossible");
                deconstructingState = State.ERROR;
                return;
            }
           
            // Loop while there is something in the input slot with sufficient amount
            while(inputInventory.getStackInSlot(0) != null 
                  && amountRequired > 0 
                  && amountRequired <= inputInventory.getStackInSlot(0).stackSize)
            {                              
                if(!outputInventory.isEmpty())
                {
                    for(int i = 0; i < outputInventory.getSizeInventory(); i++ )
                    {
                        ItemStack itemStackInOutputSlot = outputInventory
                              .getStackInSlot(i);

                        if (itemStackInOutputSlot != null 
                              && outputItemStackArray[i] != null)
                        {
                            if (!itemStackInOutputSlot.isItemEqual(
                              outputItemStackArray[i]))
                            {
                                if(!playerInventory.addItemStackToInventory(
                                      itemStackInOutputSlot))
                                 {
                                     EntityItem entityItem = playerInventory.player
                                           .entityDropItem(itemStackInOutputSlot, 0.5f);
                                     entityItem.posX = playerInventory.player.posX;
                                     entityItem.posY = playerInventory.player.posY;
                                     entityItem.posZ = playerInventory.player.posZ;
                                 }
                                 outputInventory.setInventorySlotContents(i, null);
                             }
                        }
                    }
                }

                for(int i = 0; i < outputItemStackArray.length; i++ )
                {
                    ItemStack outputItemStack = outputItemStackArray[i];
                    ItemStack currentStack = outputInventory.getStackInSlot(i);
                    if (outputItemStack != null)
                    {
                        int metadata = outputItemStack.getItemDamage();
                        if(metadata == 32767)
                        {
                            metadata = 0;
                        }
                        ItemStack newStack = null;
                        if(currentStack != null && 1 + currentStack.stackSize 
                              <= outputItemStack.getMaxStackSize())
                        {
                            newStack = new ItemStack(outputItemStack.getItem(), 
                                  1 + currentStack.stackSize, metadata);
                        }
                        else
                        {
                            if(currentStack != null && !playerInventory
                                  .addItemStackToInventory(currentStack))
                            {
                                EntityItem entityItem = playerInventory.player
                                      .entityDropItem(currentStack, 0.5f);
                                entityItem.posX = playerInventory.player.posX;
                                entityItem.posY = playerInventory.player.posY;
                                entityItem.posZ = playerInventory.player.posZ;
                            }
                            newStack = new ItemStack(outputItemStack.getItem(), 
                                  1, metadata);
                        }
                        outputInventory.setInventorySlotContents(i, newStack);
                    }
                }

                playerInventory.player.addStat(BlockSmith.deconstructedItemsStat, 
                      amountRequired);
                playerInventory.player.triggerAchievement(BlockSmith.deconstructAny);
                
                inputInventory.decrStackSize(0, amountRequired);
            }
        }
        else
        {
            resultString = I18n.format("deconstructing.result.impossible");
            deconstructingState = State.ERROR;
        }
    }

    @Override
    public ItemStack slotClick(
          int parSlotId, 
          int parMouseButtonId, 
          int parClickMode, 
          EntityPlayer parPlayer
          )
    {
        ItemStack clickItemStack = super.slotClick(parSlotId, parMouseButtonId, 
              parClickMode, parPlayer);
        if(inventorySlots.size() > parSlotId && parSlotId >= 0)
        {
            if(inventorySlots.get(parSlotId) != null)
            {
                if(((Slot) inventorySlots.get(parSlotId)).inventory == inputInventory)
                {
                    onCraftMatrixChanged(inputInventory);
                }
            }
        }
        return clickItemStack;
    }

    /**
     * Callback for when the crafting gui is closed.
     */
    @Override
    public void onContainerClosed(EntityPlayer parPlayer)
    {
        if(playerInventory.getItemStack() != null)
        {
            parPlayer.entityDropItem(playerInventory.getItemStack(), 0.5f);
        }
        if(!worldObj.isRemote)
        {
            ItemStack itemStack = inputInventory.getStackInSlotOnClosing(0);
            if(itemStack != null)
            {
                parPlayer.entityDropItem(itemStack, 0.5f);
            }

            for(int i = 0; i < outputInventory.getSizeInventory(); i++ )
            {
                itemStack = outputInventory.getStackInSlotOnClosing(i);

                if(itemStack != null)
                {
                    parPlayer.entityDropItem(itemStack, 0.5f);
                }
            }
        }
    }

    @Override
    public boolean canInteractWith(EntityPlayer player)
    {
        return true;
    }

    /**
     * Called when a player shift-clicks on a slot.
     */
    @Override
    public ItemStack transferStackInSlot(EntityPlayer parPlayer, int parSlotIndex)
    {
        Slot slot = (Slot) inventorySlots.get(parSlotIndex);
        // If there is something in the stack to pick up
        if (slot != null && slot.getHasStack())
        {
            // If the slot is one of the custom slots
            if (slot.inventory.equals(inputInventory) || slot.inventory
                   .equals(outputInventory))
            {
                // try to move to player inventory
                if (!playerInventory.addItemStackToInventory(slot.getStack()))
                {
                    return null;
                }
                slot.putStack(null);
                slot.onSlotChanged();
            }
            // if the slot is a player inventory slot
            else if(slot.inventory.equals(playerInventory))
            {
                // DEBUG
                System.out.println("Shift-clicked on player inventory slot");
                // Try to transfer to input slot
                if (!((Slot)inventorySlots.get(inputSlotNumber)).getHasStack())
                {
                    ((Slot)inventorySlots.get(inputSlotNumber)).putStack(slot.getStack());
                    slot.putStack(null);
                    slot.onSlotChanged();
                }
                else
                {
                    // DEBUG
                    System.out.println("There is already something in the input slot");
                }
            }
        }
        return null;
    }

    @Override
    public boolean canMergeSlot(ItemStack parItemStack, Slot parSlot)
    {
        return !parSlot.inventory.equals(outputInventory);
    }

    @Override
    public Slot getSlot(int parSlotIndex)
    {
        if(parSlotIndex >= inventorySlots.size())
            parSlotIndex = inventorySlots.size() - 1;
        return super.getSlot(parSlotIndex);
    }

}

Okay, so there is a lot going on in this code. Let me try to explain the key points.

In the field declarations for the class, it is straight forward: there are fields for the inventories and slots, as well as some fields to hold error codes (this is optional, but I use these to give information to user in the GUI).

In the constructor, I instantiate a recipe handler. I explain how to make the recipe handler class later.

The constructor then creates all the slots and associating them to the inventories.

Key Point: The location of the slots in the GUI is specified here in the container constructor (not in the GUI class). That is why there is some math in the slot creation -- it is creating a matrix of output slots, putting player slots along the bottom, etc. Adjust this according to how you want it laid out in your GUI.

Key Point: The onCraftMatrixChanged() method is a key method to make your machine have custom behavior.

The onCraftMatrixChanged() method is automatically called whenever someone adds or takes out something from a slot. This is where most of the interesting behavior of your machine should be coded. In this case, it checks the input stack for any recipes that match and also checks that there is sufficient quantity of the item (since some crafting can produce multiple of an item, those recipes will need that multiple to deconstruct).

I also have implemented a system of error messages using the resultString and deconstructingState fields. This isn't necessary, but I think it is nice -- if someone puts in something but it doesn't have a recipe, the GUI will display a message. And if they put in something that has a recipe but requires more of the item, it will tell them.

If you follow along in the onCraftMatrixChanged() code, it is a simple sequence -- it checks if there is something in the input stack, then checks if whatever is in the input slot has enough of something that has a recipe, and if it does it uses the recipe manager to put stuff into the output slots and delete the amount from the input slot.

Create A Custom GuiContainer Class


Now what you've all been waiting for -- the GUI itself. You'll need something like this:

@SideOnly(Side.CLIENT)
public class GuiDeconstructor extends GuiContainer
{

    public ContainerDeconstructor container;
    private final String blockName;

    public GuiDeconstructor(
          InventoryPlayer playerInventory, 
          World parWorld, 
          String parBlockName, 
          int parX, 
          int parY, 
          int parZ)
    {
        super(new ContainerDeconstructor(playerInventory, parWorld, parX, parY, parZ));
        container = (ContainerDeconstructor) inventorySlots;
        blockName = parBlockName;
    }

    @Override
    public void actionPerformed(GuiButton button)  
    {
    }

    @Override
    public void drawScreen(int par1, int par2, float par3)
    {
        super.drawScreen(par1, par2, par3);
    }

    @Override
    protected void drawGuiContainerForegroundLayer(int par1, int par2)
    {
        GL11.glDisable(GL11.GL_LIGHTING);

        fontRendererObj.drawString(blockName, xSize / 2 - 
              fontRendererObj.getStringWidth(blockName) / 2 + 1, 5, 4210752);
        fontRendererObj.drawString(I18n.format("container.inventory"), 6, ySize - 96 + 2, 
              4210752);

        String string = container.resultString;
        if(string != null)
        {
            State msgType = container.deconstructingState;
            EnumChatFormatting format = EnumChatFormatting.GREEN;
            EnumChatFormatting shadowFormat = EnumChatFormatting.DARK_GRAY;
            if(msgType == ContainerDeconstructor.State.ERROR)
            {
                format = EnumChatFormatting.WHITE;
                shadowFormat = EnumChatFormatting.DARK_RED;
            }

            fontRendererObj.drawString(shadowFormat + string + EnumChatFormatting.RESET, 
                  6 + 1, ySize - 95 + 2 - fontRendererObj.FONT_HEIGHT, 0);

            fontRendererObj.drawString(format + string + EnumChatFormatting.RESET, 6, 
                  ySize - 96 + 2 - fontRendererObj.FONT_HEIGHT, 0);
        }

        GL11.glEnable(GL11.GL_LIGHTING);
    }

    @Override
    protected void drawGuiContainerBackgroundLayer(float f, int i, int j)
    {
        GL11.glPushMatrix();
        GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f);

        mc.renderEngine.bindTexture(
              new ResourceLocation("blocksmith:textures/gui/container/deconstructor.png"));

        int k = width / 2 - xSize / 2;
        int l = height / 2 - ySize / 2;
        drawTexturedModalRect(k, l, 0, 0, xSize, ySize);
        GL11.glPopMatrix();
    }
}

I didn't do much here, but you can use these methods to do a lot more. You could for instance have other buttons to do things, draw images in the background or foreground, and even change the GUI based on information from the container.

What I did do is to draw the error messages from my container -- telling the user that they need more quantity, or that an input item doesn't have a recipe.

Create And Properly File Your GUI Image Asset


So of course this whole tutorial is about making a GUI and we can't forget to make the actual image used in the GUI.

If you want something that looks like vanilla machine GUIs, like crafting table, you should take the vanilla PNG file for that GUI and edit it in an image editing program. You can copy the slots and arrange them how you want.

Then you need to import that PNG file into your resources package. The path should be the same as in the GuiContainer class' drawContainerBackgroundLayer() method. In my case, the filing path was "blocksmith:textures/gui/container/deconstructor.png".

Create A Custom IGuiHandler Class


It is very important to implement an IGuiHandler if you are using a container, as it will then provide the synchronization of the slot contents between the server and client and lets you avoid having to create custom packets for this. Basically it provides an association between the Container on the server side and the GuiScreen on the client side.

Key Point: You only need one IGuiHandler class to handle all of your container-based GUIs.

So something like this:

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.TANNING_RACK.ordinal())
            {
                return new ContainerTanningRack(player.inventory, (IInventory)tileEntity);
            }
            if (ID == BlockSmith.GUI_ENUM.FORGE.ordinal())
            {
                return new ContainerForge(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.TANNING_RACK.ordinal())
            {
                return new GuiTanningRack(player.inventory, (IInventory)tileEntity);
            }
            if (ID == BlockSmith.GUI_ENUM.FORGE.ordinal())
            {
                return new GuiForge(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;
    }
}

This should be mostly self-explanatory. Basically I have some blocks that work over time and therefore have tile entities, and I have the block from this tutorial which does not. In each case I check the enumerated ID that is passed into the methods and return the associated element (Container on server side, or GuiScreen on client side).

Register The IGuiHandler Class


It is important to remember to register your IGuiHandler. You do that in the init handling method of your common proxy. The registration is simply:


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

Create A Recipe Handling System


A recipe system simply is a class that can help map input slots in your container to output slots. You can try to reuse the built in recipe system, or create your own. But again it can be very simple -- just have methods that check inputs for valid contents and return the output.

In this example though I'm going to use a very complicated recipe system because I'm making a deconstructor -- basically I have to do reverse crafting. But the trick is I have to reverse all types of recipe (shaped and shapeless). Furthermore, I want to allow partial quantities -- for example, I want to be able to deconstruct a single door. Lastly, for fun I want to deconstruct some things like horse armor which don't have crafting recipes.

So I broke up my recipe handler into several classes:

  1. DeconstructingRecipeHandler is the main class whose methods are called by the Container.
  2. DeconstructingInputQuantity which helps confirm that sufficient amount of input items is in the input slot.
  3. DeconstructingAdjustedRecipes helps for those recipes where I want to allow partial deconstruction.
  4. DeconstructingAddedRecipes helps for those recipes that normally don't exist but I added for fun.
So here we go. The DeconstructingRecipeHandler class:


public final class DeconstructingRecipeHandler
{
    // The item input to deconstructor
    public Item theItem = null;

    public DeconstructingAdjustedRecipes deconstructingAdjustedRecipes = 
          new DeconstructingAdjustedRecipes();
    
    public DeconstructingRecipeHandler()
    {
        
    }
    
    public ItemStack[] getDeconstructResults(ItemStack parItemStack)
    {
        // DEBUG
        System.out.println("Looking for deconstructing a recipe for "
              + parItemStack.getUnlocalizedName());
        
        // Allow recipes for some vanilla items that normally don't have recipes
        theItem = parItemStack.getItem();
        if (DeconstructingAddedRecipes.shouldAddRecipe(theItem))
        {
            return DeconstructingAddedRecipes.getCraftingGrid(theItem);
        }

        // check all recipes for recipe for Itemstack
        List<?> listAllRecipes = CraftingManager.getInstance().getRecipeList();
                
        for(int i = 0;i<listAllRecipes.size();i++)
        {
            IRecipe recipe = (IRecipe) listAllRecipes.get(i);
            if(recipe != null)
            {
                ItemStack recipeKeyItemStack = recipe.getRecipeOutput();
                if(recipeKeyItemStack!=null)
                {
                    if (recipeKeyItemStack.getUnlocalizedName().equals(
                          parItemStack.getUnlocalizedName()))
                    {
                        return getCraftingGrid(recipe);
                    }
                }
            }
        }
        return null;
    }
    
    public ItemStack[] getCraftingGrid(IRecipe parRecipe)
    {
        // Initialize the result array
        ItemStack[] resultItemStackArray = new ItemStack[9];
        for(int j = 0;j<resultItemStackArray.length;j++)
        {
            resultItemStackArray[j] = null;
        }
        
        if (parRecipe instanceof ShapedRecipes)
        {
            // DEBUG
            System.out.println("getCraftingGrid for shaped recipe");
            ShapedRecipes shaped = (ShapedRecipes)parRecipe;
            for(int j = 0;j<shaped.recipeItems.length;j++)
            {
                resultItemStackArray[j] = shaped.recipeItems[j];
            }
        }
        
        if (parRecipe instanceof ShapelessRecipes)
        {
            // DEBUG
            System.out.println("getCraftingGrid for shapeless recipe");
            ShapelessRecipes shapeless = (ShapelessRecipes)parRecipe;
            for(int j = 0;j<shapeless.recipeItems.size();j++)
            {
                resultItemStackArray[j] = (ItemStack) shapeless.recipeItems.get(j);
            }
        }
        
        if (parRecipe instanceof ShapedOreRecipe)
        {
            // DEBUG
            System.out.println("getCraftingGrid for shaped ore recipe");
            ShapedOreRecipe shaped = (ShapedOreRecipe)parRecipe;
            for(int j = 0;j<shaped.getInput().length;j++)
            {
                if(shaped.getInput()[j] instanceof ItemStack)
                {
                    resultItemStackArray[j] = (ItemStack) shaped.getInput()[j];
                }
                else if(shaped.getInput()[j] instanceof List)
                {
                    Object o = ((List) shaped.getInput()[j]).get(0);
                    if(o instanceof ItemStack)
                    {
                        resultItemStackArray[j] = (ItemStack)o;
                    }
                }
            }
        }
        
        if (parRecipe instanceof ShapelessOreRecipe)
        {
            ArrayList shapelessArray = ((ShapelessOreRecipe)parRecipe).getInput();
            // DEBUG
            System.out.println(
                  "getCraftingGrid for shapeless ore recipe with input array size = "
                  + shapelessArray.size());
            for(int j = 0; j<shapelessArray.size(); j++)
            {
                if(shapelessArray.get(j) instanceof ItemStack)
                {
                    resultItemStackArray[j] = (ItemStack) shapelessArray.get(j);
                }
                else if(shapelessArray.get(j) instanceof List)
                {
                    Object o = ((List)shapelessArray.get(j)).get(0);
                    if(o instanceof ItemStack)
                    {
                        resultItemStackArray[j] = (ItemStack)o;
                    }
                    else
                    {
                        // DEBUG
                        System.out.println("But list element is not an ItemStack");
                    }
                }
            }
        }

        if (BlockSmith.allowPartialDeconstructing)
        {
        return deconstructingAdjustedRecipes.adjustOutputQuantities(resultItemStackArray, 
              parRecipe.getRecipeOutput());
        }
        else
        {
            return resultItemStackArray;
        }
    }
}

So it is fairly self explanatory. The main method is the getDeconstructingResult() which checks whether there is a recipe for a given ItemStack and then calls the getCraftingGrid() method to return a crafting grid with all the deconstructed output.

There are actually two getCraftingGrid() methods. One within the class for vanilla recipes, and one within the DeconstructingAddedRecipes class for those recipes I wanted to add.

Due to some irregularities between the different types of vanilla recipe (there are technically four types), I have to handle each in the getCraftingGrid() but otherwise I'm just calling the vanilla CraftingManager class to get the recipes.

Next we have the DeconstructingInputQuantity class:


public class DeconstructingInputQuantity 
{
    public static int getStackSizeNeeded(ItemStack parItemStack)
    {
        Item theItem = parItemStack.getItem();
        // Create recipes for some things that don't normally have them
        if (theItem == Items.enchanted_book)
        {
            if (BlockSmith.allowDeconstructEnchantedBooks)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
        List<?> crafts = CraftingManager.getInstance().getRecipeList();
        for (int i = 0;i<crafts.size();i++)
        {
            IRecipe recipe = (IRecipe) crafts.get(i);
            if (recipe != null)
            {
                ItemStack outputItemStack = recipe.getRecipeOutput();
                // if found matching recipe
                if (outputItemStack != null)
                {                    
                    if (outputItemStack.getUnlocalizedName().equals(
                          parItemStack.getUnlocalizedName()))
                    {
                        // DEBUG
                        System.out.println("getStackSizeNeeded() found matching recipe");
                        return adjustQuantity(theItem, outputItemStack.stackSize);
                    }
                }
            }
        }
        // DEBUG
        System.out.println("No matching recipe found!");
        return 0; // no recipe found
    }

    public static int adjustQuantity(Item theItem, int parDefaultQuantity)
    {
        // prevent some deconstructions that aren't realistic (like paper into reeds)
        if (!BlockSmith.allowDeconstructUnrealistic)
        {
            if (     theItem == Items.paper 
                  || theItem == Items.melon_seeds
                  || theItem == Items.pumpkin_seeds
                  || theItem == Items.bread
                  || theItem == Items.cake
                )
            {
                // DEBUG
                System.out.println(
                      "Trying to deconstruct unrealistic item when not allowed");
                return 0;
            }
        }
        if (!BlockSmith.allowHorseArmorCrafting && 
                (    theItem == Items.saddle
                  || theItem == Items.iron_horse_armor
                  || theItem == Items.golden_horse_armor
                  || theItem == Items.diamond_horse_armor
                )
            )
        {
            // DEBUG
            System.out.println(
                  "Trying to deconstruct horse armor or saddle item when not allowed");
            return 0;
        }
        if (!BlockSmith.allowPartialDeconstructing)
        {
            // DEBUG
            System.out.println(
                  "Don't look for partial deconstruct recipe when not allowed");
            return parDefaultQuantity;
        }
        if (       theItem == Items.oak_door
                || theItem == Items.spruce_door
                || theItem == Items.birch_door
                || theItem == Items.jungle_door
                || theItem == Items.acacia_door
                || theItem == Items.dark_oak_door
                || theItem == Items.iron_door
                || theItem == Items.paper
                || theItem == Items.stick
                || theItem == Item.getItemFromBlock(Blocks.ladder)
                || theItem == Items.enchanted_book                    
                || theItem == Item.getItemFromBlock(Blocks.oak_fence)
                || theItem == Item.getItemFromBlock(Blocks.spruce_fence)
                || theItem == Item.getItemFromBlock(Blocks.birch_fence)
                || theItem == Item.getItemFromBlock(Blocks.jungle_fence)
                || theItem == Item.getItemFromBlock(Blocks.acacia_fence)
                || theItem == Item.getItemFromBlock(Blocks.dark_oak_fence)
                || theItem == Item.getItemFromBlock(Blocks.nether_brick_fence)
                || theItem == Items.sign
                || theItem == Items.glass_bottle
                || theItem == Item.getItemFromBlock(Blocks.cobblestone_wall)
                || theItem == Item.getItemFromBlock(Blocks.quartz_block)
                || theItem == Item.getItemFromBlock(Blocks.stained_hardened_clay)
                || theItem == Item.getItemFromBlock(Blocks.oak_stairs)
                || theItem == Item.getItemFromBlock(Blocks.spruce_stairs)
                || theItem == Item.getItemFromBlock(Blocks.birch_stairs)
                || theItem == Item.getItemFromBlock(Blocks.jungle_stairs)
                || theItem == Item.getItemFromBlock(Blocks.acacia_stairs)
                || theItem == Item.getItemFromBlock(Blocks.dark_oak_stairs)
                || theItem == Item.getItemFromBlock(Blocks.stone_stairs)
                || theItem == Item.getItemFromBlock(Blocks.sandstone_stairs)
                || theItem == Item.getItemFromBlock(Blocks.nether_brick_stairs)
                || theItem == Item.getItemFromBlock(Blocks.red_sandstone_stairs)
                || theItem == Item.getItemFromBlock(Blocks.quartz_stairs)
                || theItem == Item.getItemFromBlock(Blocks.stone_brick_stairs)
                || theItem == Item.getItemFromBlock(Blocks.brick_stairs)
                )
        {
            return 1;
        }
        if (theItem == Items.paper
                || theItem == Item.getItemFromBlock(Blocks.wooden_slab)
                || theItem == Item.getItemFromBlock(Blocks.stone_slab)
                || theItem == Item.getItemFromBlock(Blocks.stone_slab2)
                )
        {
            return 2;
        }
        if (       theItem == Item.getItemFromBlock(Blocks.iron_bars)
                || theItem == Item.getItemFromBlock(Blocks.rail)
                || theItem == Item.getItemFromBlock(Blocks.golden_rail)
                || theItem == Item.getItemFromBlock(Blocks.activator_rail)
                || theItem == Item.getItemFromBlock(Blocks.detector_rail)
                || theItem == Item.getItemFromBlock(Blocks.glass_pane)
                || theItem == Item.getItemFromBlock(Blocks.stained_glass_pane)
                )
        {
            return 8;
        }
        return parDefaultQuantity;
    }
}

Okay, this class needs some explanation. First of all, I made my mod configurable with a Configuration (I don't go into detail on that in this tutorial). So whever you see something like BlockSmith.allowDeconstructEnchantedBooks, that is a configuration option.

Furthermore, to handle the option I have for partial deconstructing (this is where you only need one of something to de-craft, even if the crafting recipe produces more than one). I handle the cases of various items and indicate how many are needed.

Next there is the DeconstructingAdjustedRecipes class:


public class DeconstructingAdjustedRecipes 
{
    // Counters to allow fractional deconstruction
    public int divideByTwoCounter = 1;
    public int divideByThreeCounter = 2;
    public int divideByFourCounter = 3;
    public int divideByEightCounter = 7;
    
    // The item and meta data input to deconstructor
    public Item theItem = null;
    public int theMetadata = 0;
    
    public DeconstructingAdjustedRecipes()
    {
        
    }

    /**
     * Adjust those cases where the recipe can be divided down (e.g. one door 
     * gives back two blocks)    
    public ItemStack[] adjustOutputQuantities(
          ItemStack[] parOutputItemStackArray, ItemStack parInputItemStack) 
    {
        theItem = parInputItemStack.getItem();
        theMetadata = theItem.getMetadata(parInputItemStack);
        if (theItem == Items.oak_door) return outputForWoodenDoor(0);
        if (theItem == Items.spruce_door) return outputForWoodenDoor(1);
        else if (theItem == Items.birch_door) return outputForWoodenDoor(2);
        else if (theItem == Items.jungle_door) return outputForWoodenDoor(3);
        else if (theItem == Items.acacia_door) return outputForWoodenDoor(4);
        else if (theItem == Items.dark_oak_door) return outputForWoodenDoor(5);
        else if (theItem == Items.iron_door)
        {
            return new ItemStack[] {
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Items.iron_ingot, 1, 0),
                    null, null, null, null, null, null, null
            };
        }
        else if (theItem == Items.paper) return outputSingle(Items.reeds);
        else if (theItem == Items.stick) return outputSingle(Blocks.planks);
        else if (theItem == Item.getItemFromBlock(Blocks.ladder))
        {
            if (divideByThreeCounter <= 0)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        null, null, null,
                        new ItemStack(Items.stick, 1, 0),
                        new ItemStack(Items.stick, 1, 0), 
                        new ItemStack(Items.stick, 1, 0), 
                        null, null, null
                };
            }
            else if (divideByThreeCounter == 1)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        new ItemStack(Items.stick, 1, 0),
                        null,
                        new ItemStack(Items.stick, 1, 0), 
                        null, null, null, null, null, null
                };
            }
            else if (divideByThreeCounter == 2)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        null, null, null, null, null, null,
                        new ItemStack(Items.stick, 1, 0),
                        null,
                        new ItemStack(Items.stick, 1, 0), 
                };
            }
        }
        else if (theItem == Item.getItemFromBlock(Blocks.oak_fence)) 
              return outputForWoodenFence(0);
        else if (theItem == Item.getItemFromBlock(Blocks.spruce_fence)) 
              return outputForWoodenFence(1);
        else if (theItem == Item.getItemFromBlock(Blocks.birch_fence)) 
              return outputForWoodenFence(2);
        else if (theItem == Item.getItemFromBlock(Blocks.jungle_fence)) 
              return outputForWoodenFence(3);
        else if (theItem == Item.getItemFromBlock(Blocks.acacia_fence)) 
              return outputForWoodenFence(4);
        else if (theItem == Item.getItemFromBlock(Blocks.dark_oak_fence)) 
              return outputForWoodenFence(5);
        else if (theItem == Items.enchanted_book)
        {
            return new ItemStack[] {
                    null, 
                    new ItemStack(Items.reeds, 1, 0),
                    null,
                    null, 
                    new ItemStack(Items.reeds, 1, 0),
                    null,
                    null, 
                    new ItemStack(Items.reeds, 1, 0),
                    null
            };
        }
        else if (theItem == Item.getItemFromBlock(Blocks.nether_brick_fence)) 
              return outputSingle(Blocks.nether_brick);
        else if (theItem == Item.getItemFromBlock(Blocks.wooden_slab)) 
              return outputSingle(Blocks.planks, theMetadata);
        else if (theItem == Item.getItemFromBlock(Blocks.stone_slab)) 
              return outputForStoneSlab();
        else if (theItem == Item.getItemFromBlock(Blocks.stone_slab2)) 
              return outputSingle(Blocks.red_sandstone);
        else if (theItem == Items.sign)
        {
            ItemStack planksItemStack = new ItemStack(
                  Item.getItemFromBlock(Blocks.planks), 1, 0);
            if (divideByThreeCounter == 2)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        planksItemStack, null, null,
                        planksItemStack, null, null, 
                        null, null, null
                };
            }
            else if (divideByThreeCounter == 0)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        null, planksItemStack, null, 
                        null, planksItemStack, null, 
                        null, new ItemStack(Items.stick, 1, 0), null
                };
            }
            else if (divideByThreeCounter == 1)
            {
                decrementDivideByThreeCounter();
                return new ItemStack[] {
                        null, null, planksItemStack, 
                        null, null, planksItemStack,
                        null, null, null
                };
            }
        }
        else if (theItem == Items.glass_bottle) return outputSingle(Blocks.glass);
        else if (theItem == Item.getItemFromBlock(Blocks.rail))
        {
            // DEBUG
            System.out.println("Divide by two counter = "+divideByTwoCounter);
            if (divideByTwoCounter == 1)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        new ItemStack(Items.iron_ingot, 1, 0), null, null,
                        new ItemStack(Items.iron_ingot, 1, 0), null, null,
                        new ItemStack(Items.iron_ingot, 1, 0), null, null
                };
            }
            else if (divideByTwoCounter == 0)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        null, null, new ItemStack(Items.iron_ingot, 1, 0),
                        null, new ItemStack(Items.stick, 1, 0), new ItemStack(
                              Items.iron_ingot, 1, 0),
                        null, null, new ItemStack(Items.iron_ingot, 1, 0)
                };
            }
        }
        else if (theItem == Item.getItemFromBlock(Blocks.golden_rail))
        {
            // DEBUG
            System.out.println("Divide by two counter = "+divideByTwoCounter);
            if (divideByTwoCounter == 1)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        new ItemStack(Items.gold_ingot, 1, 0), null, null,
                        new ItemStack(Items.gold_ingot, 1, 0), new ItemStack(
                              Items.stick, 1, 0), null,
                        new ItemStack(Items.gold_ingot, 1, 0), null, null
                };
            }
            else if (divideByTwoCounter == 0)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        null, null, new ItemStack(Items.gold_ingot, 1, 0),
                        null, null, new ItemStack(Items.gold_ingot, 1, 0),
                        null, new ItemStack(Items.redstone), new ItemStack(
                              Items.gold_ingot, 1, 0)
                };
            }
        }
        else if (theItem == Item.getItemFromBlock(Blocks.activator_rail))
        {
            // DEBUG
            System.out.println("Divide by two counter = "+divideByTwoCounter);
            if (divideByTwoCounter == 1)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        new ItemStack(Items.iron_ingot, 1, 0), new ItemStack(
                              Items.stick, 1, 0), null,
                        new ItemStack(Items.iron_ingot, 1, 0), null, null,
                        new ItemStack(Items.iron_ingot, 1, 0), null, null
                };
            }
            else if (divideByTwoCounter == 0)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        null, null, new ItemStack(Items.iron_ingot, 1, 0),
                        null, new ItemStack(Item.getItemFromBlock(
                              Blocks.redstone_torch), 1, 0), new ItemStack(
                                    Items.iron_ingot, 1, 0),
                        null, new ItemStack(Items.stick, 1, 0), new ItemStack(
                              Items.iron_ingot, 1, 0)
                };
            }
        }
        else if (theItem == Item.getItemFromBlock(Blocks.detector_rail))
        {
            // DEBUG
            System.out.println("Divide by two counter = "+divideByTwoCounter);
            if (divideByTwoCounter == 1)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        new ItemStack(Items.iron_ingot, 1, 0), null, null,
                        new ItemStack(Items.iron_ingot, 1, 0), new ItemStack(
                              Item.getItemFromBlock(Blocks.stone_pressure_plate), 1, 0), 
                              null,
                        new ItemStack(Items.iron_ingot, 1, 0), null, null
                };
            }
            else if (divideByTwoCounter == 0)
            {
                decrementDivideByTwoCounter();
                return new ItemStack[] {
                        null, null, new ItemStack(Items.iron_ingot, 1, 0),
                        null, null, new ItemStack(Items.iron_ingot, 1, 0),
                        null, new ItemStack(Items.redstone), new ItemStack(
                              Items.iron_ingot, 1, 0)
                };
            }
        }
        else if (theItem == Item.getItemFromBlock(Blocks.glass_pane))
        {
            return new ItemStack[] {
                    null, null, null, null, null, null,
                    new ItemStack(Item.getItemFromBlock(Blocks.glass), 1, 0), 
                    new ItemStack(Item.getItemFromBlock(Blocks.glass), 1, 0), 
                    new ItemStack(Item.getItemFromBlock(Blocks.glass), 1, 0)
            };
        }
        else if (theItem == Item.getItemFromBlock(Blocks.stained_glass_pane))
        {
            return new ItemStack[] {
                    null, null, null, null, null, null,
                    new ItemStack(Item.getItemFromBlock(Blocks.stained_glass), 1, 
                          theMetadata), 
                    new ItemStack(Item.getItemFromBlock(Blocks.stained_glass), 1, 
                          theMetadata), 
                    new ItemStack(Item.getItemFromBlock(Blocks.stained_glass), 1, 
                          theMetadata)
            };
        }
        else if (theItem == Item.getItemFromBlock(Blocks.cobblestone_wall)) 
              return outputSingle(Blocks.cobblestone);
        else if (theItem == Item.getItemFromBlock(Blocks.quartz_block)) 
              return outputForQuartz();
        else if (theItem == Item.getItemFromBlock(Blocks.stained_hardened_clay)) 
              return outputForHardenedClay();
        // Wooden stairs
        else if (theItem == Item.getItemFromBlock(Blocks.oak_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 0));
        else if (theItem == Item.getItemFromBlock(Blocks.spruce_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 1));
        else if (theItem == Item.getItemFromBlock(Blocks.birch_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 2));
        else if (theItem == Item.getItemFromBlock(Blocks.jungle_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 3));
        else if (theItem == Item.getItemFromBlock(Blocks.acacia_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 4));
        else if (theItem == Item.getItemFromBlock(Blocks.dark_oak_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.planks), 1, 5));
        // Stone stairs
        else if (theItem == Item.getItemFromBlock(Blocks.sandstone_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.sandstone)));
        else if (theItem == Item.getItemFromBlock(Blocks.stone_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.cobblestone)));
        else if (theItem == Item.getItemFromBlock(Blocks.brick_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.brick_block)));
        else if (theItem == Item.getItemFromBlock(Blocks.nether_brick_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.nether_brick)));
        else if (theItem == Item.getItemFromBlock(Blocks.red_sandstone_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.red_sandstone)));
        else if (theItem == Item.getItemFromBlock(Blocks.stone_brick_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.stonebrick)));
        else if (theItem == Item.getItemFromBlock(Blocks.quartz_stairs)) 
              return outputForStairs(new ItemStack(Item.getItemFromBlock(
                    Blocks.quartz_block)));

        // else no adjustments needed
        return parOutputItemStackArray ;
    }
    
    private ItemStack[] outputSingle(Block parBlock)
    {
        return new ItemStack[] {
                new ItemStack(Item.getItemFromBlock(parBlock)),
                null, null, null, null, null, null, null, null
        };
    }

    private ItemStack[] outputSingle(Item parItem)
    {
        return new ItemStack[] {
                new ItemStack(parItem),
                null, null, null, null, null, null, null, null
        };
    }
        
    private ItemStack[] outputSingle(Block parBlock, int parMetadata)
    {
        return new ItemStack[] {
                new ItemStack(Item.getItemFromBlock(parBlock), 1, parMetadata),
                null, null, null, null, null, null, null, null
        };
    }

    private ItemStack[] outputSingle(Item parItem, int parMetadata)
    {
        return new ItemStack[] {
                new ItemStack(parItem, 1, parMetadata),
                null, null, null, null, null, null, null, null
        };
    }

    private ItemStack[] outputForWoodenDoor(int parMetadata)
    {
        return new ItemStack[] {
                new ItemStack(Item.getItemFromBlock(Blocks.planks), 1, parMetadata),
                new ItemStack(Item.getItemFromBlock(Blocks.planks), 1, parMetadata),
                null, null, null, null, null, null, null
        };
    }

    private ItemStack[] outputForWoodenFence(int parMetadata)
    {
        ItemStack[] resultItemStackArray = initItemStackArray();
        ItemStack planksItemStack = new ItemStack(Item.getItemFromBlock(Blocks.planks), 
              1, parMetadata);
        if (divideByThreeCounter == 2)
        {
            decrementDivideByThreeCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null,
                    planksItemStack,
                    new ItemStack(Items.stick, 1, 0), 
                    null, null, null, null
            };
        }
        else if (divideByThreeCounter == 1)
        {
            decrementDivideByThreeCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null, null, null, null, null,
                    new ItemStack(Items.stick, 1, 0), 
                    planksItemStack
            };
        }
        else if (divideByThreeCounter == 0)
        {
            decrementDivideByThreeCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null, null, null,
                    planksItemStack,
                    planksItemStack,
                    null, null
            };
        }
        return resultItemStackArray;
    }

    
    private ItemStack[] outputForStoneSlab()
    {
        ItemStack[] resultItemStackArray = initItemStackArray();
        // Need to handle all the various subtypes
        // Also need to handle upper and lower slabs (this is why I do bitwise mask with 7)
        if ((theMetadata&7) == 0)
        {
            resultItemStackArray = new ItemStack[] {
                    new ItemStack(Item.getItemFromBlock(Blocks.stone), 1, 0),
                    null, null, null, null, null, null, null, null
            };
        }
        else if ((theMetadata&7) == 1)
        {
            resultItemStackArray = new ItemStack[] {
                    new ItemStack(Item.getItemFromBlock(Blocks.sandstone), 1, 0),
                    null, null, null, null, null, null, null, null
            };
        }
        // this is supposed to be "(stone) wooden slab" which I don't know what that is
else if ((theMetadata&7) == 2) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.stone), 1, 0), null, null, null, null, null, null, null, null }; } else if ((theMetadata&7) == 3) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.cobblestone), 1, 0), null, null, null, null, null, null, null, null }; } else if ((theMetadata&7) == 4) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.brick_block), 1, 0), null, null, null, null, null, null, null, null }; } else if ((theMetadata&7) == 5) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.stonebrick), 1, 0), null, null, null, null, null, null, null, null }; } else if ((theMetadata&7) == 6) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.nether_brick), 1, 0), null, null, null, null, null, null, null, null }; } else if ((theMetadata&7) == 7) { resultItemStackArray = new ItemStack[] { new ItemStack(Item.getItemFromBlock(Blocks.quartz_block), 1, 0), null, null, null, null, null, null, null, null }; } return resultItemStackArray; } private ItemStack[] outputForQuartz() { ItemStack[] resultItemStackArray = initItemStackArray(); if (theMetadata == 0) // regular quartz block { resultItemStackArray = new ItemStack[] { null, null, null, new ItemStack(Items.quartz, 1, 0), new ItemStack(Items.quartz, 1, 0), 
                          null,
                    new ItemStack(Items.quartz, 1, 0), new ItemStack(Items.quartz, 1, 0), 
                          null
            };
        }
        else if (theMetadata == 1) // chizeled quartz block
        {
            resultItemStackArray = new ItemStack[] {
                    null, null, null,
                    null, new ItemStack(Item.getItemFromBlock(Blocks.stone_slab), 1, 7), 
                          null,
                    null, new ItemStack(Item.getItemFromBlock(Blocks.stone_slab), 1, 7), 
                          null
            };
        }
        // pillar quartz block, any orientation
        else if (theMetadata == 2 || theMetadata == 3 || theMetadata == 4) 
        {
            if (divideByTwoCounter == 1)
            {
                decrementDivideByTwoCounter();
                resultItemStackArray = new ItemStack[] {
                        null, null, null,
                        null, null, null,
                        null, new ItemStack(Item.getItemFromBlock(Blocks.quartz_block), 
                              1, 0), null
                };
            }
            else if (divideByTwoCounter == 0)
            {
                decrementDivideByTwoCounter();
                resultItemStackArray = new ItemStack[] {
                        null, null, null,
                        null, new ItemStack(Item.getItemFromBlock(Blocks.quartz_block), 
                              1, 0), null,
                        null, null, null
                };
            }
        }
        return resultItemStackArray;
    }

    private ItemStack[] outputForHardenedClay()
    {
        if (divideByEightCounter != 3) 
        {
            decrementDivideByEightCounter();
            return new ItemStack[] {
                    new ItemStack(Item.getItemFromBlock(Blocks.clay), 1, 0), null, null,
                    null, null, null,
                    null, null, null
            };
        }
        else 
        {
            // DEBUG
            System.out.println("Should output a dye");
            decrementDivideByEightCounter();
            return new ItemStack[] {
                    new ItemStack(Item.getItemFromBlock(Blocks.clay), 1, 0), 
                          new ItemStack(Items.dye, 1, 
                                convertClayMetaToDyeMeta(theMetadata)), null,
                    null, null, null,
                    null, null, null
            };
        }
    }
    
    private ItemStack[] outputForStairs(ItemStack parOutputItemStack)
    { 
        ItemStack[] resultItemStackArray = initItemStackArray();
        if (divideByFourCounter == 0) 
        {
            decrementDivideByFourCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, parOutputItemStack,
                    null, null, null,
                    null, null, null
            };
        }
        else if (divideByFourCounter == 1)
        {
            decrementDivideByFourCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null,
                    null, parOutputItemStack, parOutputItemStack,
                    null, null, null
            };
        }
        else if (divideByFourCounter == 2)
        {
            decrementDivideByFourCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null,
                    null, null, null,
                    null, null, parOutputItemStack
            };
        }
        else if (divideByFourCounter == 3)
        {
            decrementDivideByFourCounter();
            resultItemStackArray = new ItemStack[] {
                    null, null, null,
                    null, null, null,
                    parOutputItemStack, parOutputItemStack, null
            };
        }
    
        return resultItemStackArray;
    }

    private int convertClayMetaToDyeMeta(int parClayMeta)
    {
        // for some reason dye and clay have reversed sequence of meta data values
        return 15-parClayMeta;
    }
    
    private void decrementDivideByTwoCounter()
    {
        divideByTwoCounter--;
        if (divideByTwoCounter<0)
        {
            divideByTwoCounter=1;
        }                
    }
    
    private void decrementDivideByThreeCounter()
    {
        divideByThreeCounter--;
        if (divideByThreeCounter<0)
        {
            divideByThreeCounter=2;
        }                
    }
    
    private void decrementDivideByFourCounter()
    {
        divideByFourCounter--;
        if (divideByFourCounter<0)
        {
            divideByFourCounter=3;
        }                
    }
    
    private void decrementDivideByEightCounter()
    {
        divideByEightCounter--;
        if (divideByEightCounter<0)
        {
            divideByEightCounter=7;
        }                
    }
    
    private ItemStack[] initItemStackArray()
    {
        ItemStack[] resultItemStackArray = new ItemStack[9];
        for(int j = 0;j<resultItemStackArray.length;j++)
        {
            resultItemStackArray[j] = null;
        }
        return resultItemStackArray;
    }
}

This one gets interesting. Basically, in the case where the user has set the configuration to allow partial deconstruction, this class is used to figure out what the output should be. It is very complicated because in some recipes the input items are not evenly divisible by the output number. In that case I decided to make it so that on average you get the right material back but in stages. So I create counters for each case of output quantity and use these to figure out what part of the recipe input should be given back when deconstructing.

Tip: Sometimes to make a feature you want in a mod it isn't possible to make a regular algorithm to handle it and instead you just have to type out every case you need to handle. Don't be lazy -- sometimes just typing out all the cases is easier than trying to figure out a more clever solution.

And lastly the DeconstructingAddedRecipes class:


public class DeconstructingAddedRecipes 
{
    public static boolean shouldAddRecipe(Item parItem)
    {
        return 
                (  parItem == Items.enchanted_book
                || parItem == Items.iron_horse_armor 
                || parItem == Items.golden_horse_armor 
                || parItem == Items.diamond_horse_armor 
                );
    }
    
    public static ItemStack[] getCraftingGrid(Item parItem)
    {
        // Initialize the result array
        ItemStack[] resultItemStackArray = new ItemStack[9];
        for(int j = 0;j<resultItemStackArray.length;j++)
        {
            resultItemStackArray[j] = null;
        }
        
        // Create deconstructing recipes for things that don't have crafting recipes
        if (parItem == Items.enchanted_book)
        {
            resultItemStackArray = new ItemStack[] {
                    null, new ItemStack(Items.leather, 1, 0), null,
                    new ItemStack(Items.paper, 1, 0), new ItemStack(Items.paper, 1, 0),  
                          new ItemStack(Items.paper, 1, 0),
                    null, null, null
            };
        }
        // Even though horse armor has recipe, need to adjust the wool color when 
        // deconstructed
        else if (parItem == Items.iron_horse_armor)
        {
            return new ItemStack[] {
                    null,
                    null,
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Item.getItemFromBlock(Blocks.wool), 1, 15),
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Items.iron_ingot, 1, 0),
                    new ItemStack(Items.iron_ingot, 1, 0)
            };
        }
        else if (parItem == Items.golden_horse_armor)
        {
            return new ItemStack[] {
                    null,
                    null,
                    new ItemStack(Items.gold_ingot, 1, 0),
                    new ItemStack(Items.gold_ingot, 1, 0),
                    new ItemStack(Item.getItemFromBlock(Blocks.wool), 1, 12),
                    new ItemStack(Items.gold_ingot, 1, 0),
                    new ItemStack(Items.gold_ingot, 1, 0),
                    new ItemStack(Items.gold_ingot, 1, 0),
                    new ItemStack(Items.gold_ingot, 1, 0)
            };
        }
        else if (parItem == Items.diamond_horse_armor)
        {
            return new ItemStack[] {
                    null,
                    null,
                    new ItemStack(Items.diamond, 1, 0),
                    new ItemStack(Items.diamond, 1, 0),
                    new ItemStack(Item.getItemFromBlock(Blocks.wool), 1, 11),
                    new ItemStack(Items.diamond, 1, 0),
                    new ItemStack(Items.diamond, 1, 0),
                    new ItemStack(Items.diamond, 1, 0),
                    new ItemStack(Items.diamond, 1, 0)
            };
        }
        return resultItemStackArray;
    }
}

This class is pretty simple. Basically it is just defining the deconstruction recipes for some things that normally don't have recipes. The getCraftingGrid() is called by the DecontsructingRecipeHandler class.

Tip: For your own machine, this type of class might be all you need for the recipes.

Conclusion


Whew, that's it! I was really happy with the result of this deconstructing machine. I'll try to post a video soon to make it more clear how some of the trickier bits are working.

For your own machine, you can make the recipe portion simpler, but I've seen a lot of people ask about making a "de-crafting" table so figured it made an interesting (but complicated) tutorial.

As always, please feel free to comment or let me know any errors that need correction. Happy modding!

14 comments:

  1. Great tutorial. Way over my head for the more simple machine we are trying to create. We have a simple construct of a crafting bench with a 1x5 grid input and a single result slot. We have the recipe match working, i.e., we can put items in the slots and get out our result. The challenge is tht the input stacks won't decrement when we pull out the result. The action seems to be defined in the onCraftMatrixChanged method of the container, which is where the call to the recipe match is. Any pointers on where to look for this action. I can post our code if it will help. Thanks in advance.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. I'm having a problem in 1.7.10:

    As soon as the recipe is valid, it insta-consumes my input items and leaves the output. I want it to wait until I click the output (like a vanilla table would). How do I fix that?

    ReplyDelete
  4. Hey, I am working on a mod, and I want to build a simple workbench with a 4x4 input. But I don't know how to do it, could you make a tutorial? Thank you.

    ReplyDelete
    Replies
    1. Sorry I'm really busy right now. I think the information above should help you figure out the GUI part of it, so the only tricky part would be the recipe processing. If it is a shaped recipe, it is pretty easy to code tests to make sure the right things are in the slots (i.e. just check for logical and of whether what is in each slot matches the recipe). If it is a shapeless recipe, it is also easy to code -- just count up the quantities of each thing to see if they match the quantities expected for the recipe.

      After you make the recipe match, it should work the same as the crafting table. You'd remove the items from the input slots and put the result in the output slot.

      Delete
  5. Hi,
    My GUI doesn't open but I get a message in the console log that it is.How can i fix it? What am I doing wrong in the code?

    ReplyDelete
  6. I'm having trouble understanding the 1.10.2 version of Container.slotClick. Can you help?

    ReplyDelete
  7. Your blog is extremely brilliant. Quality contents are here. Realistic Minecraft

    ReplyDelete
  8. Thanks for sharing this information It's really help Full Blog Learn Vietnamese it will help to Learners get a good understanding of basic grammar for Vietnamese, supply basic Learn Vietnamese vocabulary, then be able to start basic communication with natives.

    ReplyDelete
  9. 소라넷과 한때 최고의 어깨를 나란히 했던 무료야동 아시나요? 최고의 무료 뽀르노 사이트 추천 합니다 날마다 새로운 야동으로 업데이트 되는 최신 야설 한국야동 일본야동 중국야동 베트남야동 서양야동 백마야동 득실거리는 사이트를 찾았습니다 예전 섹스코리아” 운영자가 다시 운영 하는 사이트 라고 합니다 믿고 방문해도 좋을 듯 합니다 매번 뭣좀 볼려면 끊기거나 버퍼링 생기는 그런 사이트 아니고 자체 서버로 서비스 하더군요 품질좋고 최신 뽀르노로 가득 합니다 강추 사이트 www.sexkorea.tv

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

    ReplyDelete

  11. Hello,
    Thanks for sharing this Blog with us. Custom branded Marijuana Packaging for dispensaries, processors, extracts, growers and any other related company. We provide mylar bags, pop top bottles, Wax containers and any other packaging used in the cannabis industry. Add your logo with a label or get your logo printed directly on the packaging if you meet the minimum order quantity.
    Thank You.

    ReplyDelete
  12. Hello,
    This blog is really help full for me. Thanks for sharing this article with us.
    List your business online or Find your best services providers South Africa now Book appointment online .
    Thank You.

    ReplyDelete
  13. Thanks for sharing this information with us. It's very helpful for us and if you are looking for your best IPO tutorial & learning school, then please Welcome to W3Teachers.com tutorials section. Initial Pubic Offer tutorials are broken down into sections where each section contains a number of related topics (IPO) that are packed with easy to understand explanations, real-world examples, tips, notes and useful references

    ReplyDelete