12 KiB
metadata.title = "Tile Entities with Inventory"
metadata.date = "2016-11-27 14:25:42 -0400"
metadata.series = "forge-modding-112"
metadata.seriesName = "Forge Mods for 1.12"
Now that we've learned the basics of making tile entities, let's make a more complicated one that has an inventory. We'll be modifying our existing Pedestal block so that it can actually store an item.
Note: If you haven't already completed the tile entities tutorial, you'll want to do that so you'll have the foundations that this tutorial builds on.
The Block
Firstly, we'll move the BlockPedestal
from the block
package to the block.pedestal
package. Next, we'll change BlockPedestal
so it extends BlockTileEntity
instead of BlockBase
. We'll also specify a generic type parameter of TileEntityPedestal
, which will be the tile entity class for our pedestal. Next, we'll need to implement the abstract methods provided by BlockTileEntity
(getTileEntityClass
and createTileEntity
):
// ...
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
// ...
@Override
public Class<TileEntityPedestal> getTileEntityClass() {
return TileEntityPedestal.class;
}
@Nullable
@Override
public TileEntityPedestal createTileEntity(World world, IBlockState state) {
return new TileEntityPedestal();
}
}
From the getTileEntityClass
method, we'll return TileEntityPedestal.class
(this will cause errors because we haven't creeated the tile entity class yet) and from the createTileEntity
method, we'll return a new instance of the TileEntityPedestal
class.
Next, we'll add the onBlockActivated
method which will handle our block being right-clicked. The logic for this method will be something like this:
- Check that we're running on the server (see the Sides section of the previous tutorial).
- Retrieve the
TileEntity
and theIItemHandler
instance. - If the player is sneaking:
- If the player's hand is empty:
- Take what's in the pedestal's
IItemHandler
and put it in the player's hand.
- Take what's in the pedestal's
- Otherwise:
- Take what's in the player's hand and attempt to insert it into the pedestal
- Mark the tile entity as dirty so Minecraft knows it needs to be saved to disk.
- If the player's hand is empty:
- Otherwise:
- Retrieve the
ItemStack
currently in the pedestal - If there is a stack (i.e. it
!stack.isEmpty()
)- Send a chat message to the player with count and name of the item.
- Otherwise
- Send a chat message to the player telling them that the pedestal's empty
- Retrieve the
- Retrieve the
- Return
true
// ...
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
// ...
@Override
public boolean onBlockActivated(World world, BlockPos pos, IBlockState state, EntityPlayer player, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ) {
if (!world.isRemote) {
TileEntityPedestal tile = getTileEntity(world, pos);
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, side);
if (!player.isSneaking()) {
if (heldItem.isEmpty()) {
player.setHeldItem(hand, itemHandler.extractItem(0, 64, false));
} else {
player.setHeldItem(hand, itemHandler.insertItem(0, heldItem, false));
}
tile.markDirty();
} else {
ItemStack stack = itemHandler.getStackInSlot(0);
if (!stack.isEmpty()) {
String localized = TutorialMod.proxy.localize(stack.getUnlocalizedName() + ".name");
player.addChatMessage(new TextComponentString(stack.getCount() + "x " + localized));
} else {
player.addChatMessage(new TextComponentString("Empty"));
}
}
}
return true;
}
// ...
}
The IItemHandler
and capability stuff might look a bit confusing, but that's ok, it will be explained in more detail later on. For now, suffice it to say that the IItemHandler
is the object that stores the pedestal's inventory.
Before we can continue, we'll also need to add a new method to our proxy class. This method will take an unlocalized name (e.g. item.diamond.name
) and translate it into the correct version (e.g. Diamond
). This needs to be a method in our proxy class because there are two different ways of localizing things depending if you're on the client or the server. If you're on the server, you need to use net.minecraft.util.text.translation.I18n
whereas if you're on the client, you need to use net.minecraft.client.resources.I18n
. In our CommonProxy
class, we'll add the server-side version of this:
// ...
import net.minecraft.util.text.translation.I18n;
public class CommonProxy {
// ...
public String localize(String unlocalized, Object... args) {
return I18n.translateToLocalFormatted(unlocalized, args);
}
}
And in the ClientProxy
, we'll add the client-side version of this:
// ...
import net.minecraft.client.resources.I18n;
public class ClientProxy extends CommonProxy {
// ...
@Override
public String localize(String unlocalized, Object... args) {
return I18n.format(unlocalized, args);
}
}
Make sure you're importing the right I18n
in the right proxy class, otherwise you could cause a crash when your mod is running on a server.
The very last thing we'll need to add to our block class is the breakBlock
method. This method is called when our block is destroyed in the world, and we'll use it to drop the contents of the pedestal's inventory.
// ...
public class BlockPedestal extends BlockTileEntity<TileEntityPedestal> {
// ...
@Override
public void breakBlock(World world, BlockPos pos, IBlockState state) {
TileEntityPedestal tile = getTileEntity(world, pos);
IItemHandler itemHandler = tile.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, EnumFacing.NORTH);
ItemStack stack = itemHandler.getStackInSlot(0);
if (!stack.isEmpty()) {
EntityItem item = new EntityItem(world, pos.getX(), pos.getY(), pos.getZ(), stack);
world.spawnEntityInWorld(item);
}
super.breakBlock(world, pos, state);
}
// ...
}
In the break block method, we'll:
- Get the tile entity instance, the
IItemHandler
instance, and theItemStack
currently stored in its inventory. - If there is a stack (i.e.
!stack.isEmpty()
):- Create a new
EntityItem
instance at the correct position with the stack - Spawn the entity in the world so the item is dropped
- Create a new
- Call the
super.breakBlock
method to remove our block and tile entity from the world.
The Tile Entity
Like in the previous tutorial, the tile entity class itself will be fairly simple. This is possible because of Forge's IItemHandler
capability and its ItemStackHandler
class which handles all the logic for storing items, reading/writing them to/from NBT, and inserting/extracting items.
Capabilities
Forge provides a simple Entity Component System called capabilities. Capabilities allow mod developers to easily add/use functionality without having to implement lots of interfaces or perform lots of instanceof
checks and casts. In this tutorial we'll use the Forge-provided IItemHandler
capability which is a replacement for Vanilla's IInventory
and ISidedInventory
. We'll be using the ItemStackHandler
implementation of the capability which is provided by Forge. By overriding the hasCapability
and getCapability
methods of our tile entity, we can "register" the capabilty object and make it accessible to everyone else.
The IItemHandler
interface provides a couple methods that we can use for interacting with the inventory:
insertItem
: This method takes 3 parameters:int slot
,ItemStack stack
, andboolean simulate
and returns anItemStack
.int slot
: The index of the slot in the inventory that we want to insert into.ItemStack stack
: The stack that we are attempting to insert.boolean simulate
: If true, no modification of theIItemHandler
's internal inventory will be performed. This is useful if you want to test if an interaction can be performed.ItemStack
return: The remainder of the stack that could not be inserted. If the stack was fully inserted, this will beItemStack.EMPTY
.
extractItem
: This method takes 3 parameters:int slot
,int amount
,boolean simulate
and returns anItemStack
.int slot
: The index of the slot in the inventory that we want to extract from.int amount
: The amount of items we want to extract from the slot.boolean simulate
: If true, no modification of theIItemHandler
's internal inventory will be performed. This is useful if you want to test if an interaction can be performed.ItemStack
return: The stack that was extracted from the inventory.
Note: If you want to know more about capabilities, you can checkout the official Forge documentation on the subject.
package net.shadowfacts.tutorial.block.pedestal;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.EnumFacing;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.items.CapabilityItemHandler;
import net.minecraftforge.items.ItemStackHandler;
import javax.annotation.Nullable;
public class TileEntityPedestal extends TileEntity {
private ItemStackHandler inventory = new ItemStackHandler(1);
@Override
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setTag("inventory", inventory.serializeNBT());
return super.writeToNBT(compound);
}
@Override
public void readFromNBT(NBTTagCompound compound) {
inventory.deserializeNBT(compound.getCompoundTag("inventory"));
super.readFromNBT(compound);
}
@Override
public boolean hasCapability(Capability<?> capability, @Nullable EnumFacing facing) {
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY || super.hasCapability(capability, facing);
}
@Nullable
@Override
public <T> T getCapability(Capability<T> capability, @Nullable EnumFacing facing) {
return capability == CapabilityItemHandler.ITEM_HANDLER_CAPABILITY ? (T)inventory : super.getCapability(capability, facing);
}
}
In our tile entity, we'll have a private ItemStackHandler inventory
field which is initialized to a new ItemStackHandler(1)
. The first parameter of the ItemStackHandler
constructor is the number of slots it should have. In our case, this is 1 because the pedestal can only hold 1 stack at a time.
ItemStackHandler
also provides serializeNBT
and deserializeNBT
methods making it very easy to save our inventory. In the writeToNBT
method, we'll call inventory.serializeNBT()
to create an NBTTagCompound
that represents the inventory and set that to the key inventory
on the root compound. Similarly, in the readFromNBT
method, we'll retrieve the tag compound that has the key inventory
and pass it to inventory.deserializeNBT
so that the items that were saved to NBT are loaded back into our ItemStackHandler
object.
We'll also override the hasCapability
and getCapability
methods. In hasCapability
we'll return if the capability being tested is the IItemHandler
capability instance, or if it's provided by the super method*. Likewise, in the getCapability
method, we'll check if the capability being requested is the IItemHandler
capability and if so, return our inventory
, and otherwise, delegate to the super method*.
*: We delegate back to the super method because Forge provides an AttachCapabilitiesEvent
which allows other mods to add capabilites to tile entities and other objects that they don't own.
Lastly, we'll update our ModBlocks.register
method to register the new Pedestal tile entity.
// ...
public class ModBlocks {
// ...
public static void register(IForgeRegistry<BlocK> registry) {
// ...
GameRegistry.registerTileEntity(pedestal.getTileEntityClass(), pedestal.getRegistryName().toString());
}
// ...
}
Finished
Now we can launch Minecraft and see how our pedestal can now store an item in its inventory: