-
Notifications
You must be signed in to change notification settings - Fork 121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Block Inventory article #246
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,80 @@ | ||||||
--- | ||||||
title: Inventories | ||||||
description: Learn how to add inventories to your blocks. | ||||||
authors: | ||||||
- natri0 | ||||||
--- | ||||||
|
||||||
# Block Inventories {#inventories} | ||||||
|
||||||
In Minecraft, all blocks that can store items have an `Inventory`. This includes blocks like chests, furnaces, and hoppers. | ||||||
|
||||||
In this tutorial we'll create a block that uses its inventory to duplicate any items placed in it. | ||||||
|
||||||
## Creating the Block {#creating-the-block} | ||||||
|
||||||
This should be familiar to the reader if they've followed the [Creating Your First Block](../blocks/first-block) and [Block Entities](../blocks/block-entities) guides. We'll create a `DuplicatorBlock` that extends `BlockWithEntity` and implements `BlockEntityProvider`. | ||||||
|
||||||
@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java) | ||||||
|
||||||
Then, we need to create a `DuplicatorBlockEntity`, which needs to implement the `Inventory` interface. Thankfully, there's a helper called `ImplementedInventory` that does most of the work, leaving us with just a few methods to implement. | ||||||
|
||||||
@[code transcludeWith=:::1](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java) | ||||||
|
||||||
The `items` list is where the inventory's contents are stored. For this block we have it set to a size of 1 slot for the input. | ||||||
|
||||||
Don't forget to register the block and block entity in their respective classes! | ||||||
|
||||||
### Saving & Loading {#saving-loading} | ||||||
|
||||||
Just like with regular `BlockEntities`, if we want the contents to persist between game reloads, we need to save it as NBT. Thankfully, Mojang provides a helper class called `Inventories` with all the necessary logic. | ||||||
|
||||||
@[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java) | ||||||
|
||||||
## Interacting with the Inventory {#interacting-with-the-inventory} | ||||||
|
||||||
Technically, the inventory is already functional. However, to insert items, we currently need to use hoppers. Let's make it so that we can insert items by right-clicking the block. | ||||||
|
||||||
To do that, we need to override the `onUseWithItem` method in the `DuplicatorBlock`: | ||||||
|
||||||
@[code transcludeWith=:::2](@/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java) | ||||||
|
||||||
Here, if the player is holding an item and there is an empty slot, we move the item from the player's hand to the block's inventory and return `ItemActionResult.SUCCESS`. | ||||||
|
||||||
Now, when you right-click the block with an item, you'll no longer have an item! If you run `/data get block` on the block, you'll see the item in the `Items` field in the NBT. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
![Duplicator block & /data get block output showing the item in the inventory](/assets/develop/blocks/inventory_1.png) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
### Duplicating Items {#duplicating-items} | ||||||
|
||||||
Actually, on second thought, shouldn't a _duplicator_ block duplicate items? Let's add a `tick` method to the `DuplicatorBlockEntity` that duplicates any item in the input slot and throws it out. | ||||||
|
||||||
@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java) | ||||||
|
||||||
The `DuplicatorBlock` should now have a `getTicker` method that returns a reference to `DuplicatorBlockEntity::tick`. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need a ticking functionality here? So this could be handled in the block interaction method in the custom block class, right? Or do something like duplicating the slot every 10 ticks and then storing it in the inventory, until it's being retrieved or the stack has hit its max size. |
||||||
|
||||||
<VideoPlayer src="/assets/develop/blocks/inventory_2.mp4" /> | ||||||
|
||||||
## Sided Inventories | ||||||
Check failure on line 58 in develop/blocks/inventory.md GitHub Actions / markdownlintCustom rule
|
||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
By default, you can insert and extract items from the inventory from any side. However, this might not be the desired behavior sometimes: for example, a furnace only accepts fuel from the side and items from the top. | ||||||
|
||||||
To create this behavior, we need to implement the `SidedInventory` interface in the `BlockEntity`. This interface has three methods: | ||||||
|
||||||
- `getInvAvailableSlots(Direction)` lets you control which slots can be interacted with from a given side. | ||||||
- `canInsert(int, ItemStack, Direction)` lets you control whether an item can be inserted into a slot from a given side. | ||||||
- `canExtract(int, ItemStack, Direction)` lets you control whether an item can be extracted from a slot from a given side. | ||||||
|
||||||
Let's modify the `DuplicatorBlockEntity` to only accept items from the top: | ||||||
|
||||||
@[code transcludeWith=:::4](@/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java) | ||||||
|
||||||
The `getInvAvailableSlots` returns an array of the slot _indices_ that can be interacted with from the given side. In this case, we only have a single slot (`0`), so we return an array with just that index. | ||||||
|
||||||
Also, we should modify the `onUseWithItem` method of the `DuplicatorBlock` to actually respect the new behavior: | ||||||
|
||||||
@[code transcludeWith=:::3](@/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java) | ||||||
|
||||||
Now, if we try to insert items from the side instead of the top, it won't work! | ||||||
|
||||||
<VideoPlayer src="/assets/develop/blocks/inventory_3.webm" /> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"variants": { | ||
"": { | ||
"model": "fabric-docs-reference:block/duplicator" | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"model": { | ||
"type": "minecraft:model", | ||
"model": "fabric-docs-reference:block/duplicator" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"parent": "minecraft:block/cube_all", | ||
"textures": { | ||
"all": "fabric-docs-reference:block/duplicator" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package com.example.docs.block.custom; | ||
|
||
import com.mojang.serialization.MapCodec; | ||
import org.jetbrains.annotations.Nullable; | ||
|
||
import net.minecraft.block.BlockRenderType; | ||
import net.minecraft.block.BlockState; | ||
import net.minecraft.block.BlockWithEntity; | ||
import net.minecraft.block.entity.BlockEntity; | ||
import net.minecraft.block.entity.BlockEntityTicker; | ||
import net.minecraft.block.entity.BlockEntityType; | ||
import net.minecraft.entity.player.PlayerEntity; | ||
import net.minecraft.item.ItemStack; | ||
import net.minecraft.util.ActionResult; | ||
import net.minecraft.util.Hand; | ||
import net.minecraft.util.hit.BlockHitResult; | ||
import net.minecraft.util.math.BlockPos; | ||
import net.minecraft.world.World; | ||
|
||
import com.example.docs.block.entity.ModBlockEntities; | ||
import com.example.docs.block.entity.custom.DuplicatorBlockEntity; | ||
|
||
// :::1 | ||
public class DuplicatorBlock extends BlockWithEntity { | ||
// :::1 | ||
|
||
public DuplicatorBlock(Settings settings) { | ||
super(settings); | ||
} | ||
|
||
@Override | ||
protected MapCodec<? extends BlockWithEntity> getCodec() { | ||
return createCodec(DuplicatorBlock::new); | ||
} | ||
|
||
// :::1 | ||
@Nullable | ||
@Override | ||
public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { | ||
return new DuplicatorBlockEntity(pos, state); | ||
} | ||
|
||
// :::1 | ||
|
||
@Override | ||
protected BlockRenderType getRenderType(BlockState state) { | ||
return BlockRenderType.MODEL; | ||
} | ||
|
||
// :::2 | ||
@Override | ||
protected ActionResult onUseWithItem(ItemStack stack, BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { | ||
if (!(world.getBlockEntity(pos) instanceof DuplicatorBlockEntity duplicatorBlockEntity)) { | ||
return ActionResult.PASS_TO_DEFAULT_BLOCK_ACTION; | ||
} | ||
|
||
// :::2 | ||
|
||
// :::3 | ||
if (!duplicatorBlockEntity.canInsert(0, stack, hit.getSide())) { | ||
return ActionResult.PASS_TO_DEFAULT_BLOCK_ACTION; | ||
} | ||
|
||
// :::3 | ||
|
||
// :::2 | ||
if (!player.getStackInHand(hand).isEmpty() && duplicatorBlockEntity.isEmpty()) { | ||
duplicatorBlockEntity.setStack(0, player.getStackInHand(hand).copy()); | ||
player.getStackInHand(hand).setCount(0); | ||
} | ||
|
||
return ActionResult.SUCCESS; | ||
} | ||
|
||
// :::2 | ||
|
||
@Nullable | ||
@Override | ||
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) { | ||
return validateTicker(type, ModBlockEntities.DUPLICATOR_BLOCK_ENTITY, DuplicatorBlockEntity::tick); | ||
} | ||
|
||
// :::1 | ||
// ... | ||
} | ||
// :::1 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package com.example.docs.block.entity.custom; | ||
|
||
import org.jetbrains.annotations.Nullable; | ||
|
||
import net.minecraft.block.Block; | ||
import net.minecraft.block.BlockState; | ||
import net.minecraft.block.entity.BlockEntity; | ||
import net.minecraft.inventory.Inventories; | ||
import net.minecraft.inventory.SidedInventory; | ||
import net.minecraft.item.ItemStack; | ||
import net.minecraft.nbt.NbtCompound; | ||
import net.minecraft.registry.RegistryWrapper; | ||
import net.minecraft.util.collection.DefaultedList; | ||
import net.minecraft.util.math.BlockPos; | ||
import net.minecraft.util.math.Direction; | ||
import net.minecraft.world.World; | ||
|
||
import com.example.docs.block.entity.ModBlockEntities; | ||
import com.example.docs.inventory.ImplementedInventory; | ||
|
||
/* | ||
The following is a dummy piece of code to not have `implements SidedInventory` in the first code block where we implement `ImplementedInventory`. | ||
lmk if you have a better idea on how to handle this. | ||
// :::1 | ||
public class DuplicatorBlockEntity extends BlockEntity implements ImplementedInventory { | ||
// :::1 | ||
*/ | ||
|
||
public class DuplicatorBlockEntity extends BlockEntity implements ImplementedInventory, SidedInventory { | ||
// :::1 | ||
|
||
private final DefaultedList<ItemStack> items = DefaultedList.ofSize(1, ItemStack.EMPTY); | ||
|
||
public DuplicatorBlockEntity(BlockPos pos, BlockState state) { | ||
super(ModBlockEntities.DUPLICATOR_BLOCK_ENTITY, pos, state); | ||
} | ||
|
||
@Override | ||
public DefaultedList<ItemStack> getItems() { | ||
return items; | ||
} | ||
|
||
// :::1 | ||
|
||
// :::2 | ||
@Override | ||
protected void readNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { | ||
super.readNbt(nbt, registryLookup); | ||
Inventories.readNbt(nbt, items, registryLookup); | ||
} | ||
|
||
@Override | ||
protected void writeNbt(NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { | ||
Inventories.writeNbt(nbt, items, registryLookup); | ||
super.writeNbt(nbt, registryLookup); | ||
} | ||
|
||
// :::2 | ||
|
||
// :::3 | ||
public static void tick(World world, BlockPos blockPos, BlockState blockState, DuplicatorBlockEntity duplicatorBlockEntity) { | ||
if (!duplicatorBlockEntity.isEmpty()) { | ||
ItemStack stack = duplicatorBlockEntity.getStack(0); | ||
duplicatorBlockEntity.clear(); | ||
|
||
Block.dropStack(world, blockPos, stack); | ||
Block.dropStack(world, blockPos, stack); | ||
} | ||
} | ||
|
||
// :::3 | ||
|
||
// :::4 | ||
@Override | ||
public int[] getAvailableSlots(Direction side) { | ||
return new int[]{ 0 }; | ||
} | ||
|
||
@Override | ||
public boolean canInsert(int slot, ItemStack stack, @Nullable Direction dir) { | ||
return dir == Direction.UP; | ||
} | ||
|
||
@Override | ||
public boolean canExtract(int slot, ItemStack stack, Direction dir) { | ||
return true; | ||
} | ||
|
||
// :::4 | ||
|
||
// :::1 | ||
} | ||
// :::1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be just me, but this kind of makes it sound like the
Inventory
interface is always needed to store any kind of ItemStack. Maybe mention it as "best practice" or something?