Skip to content
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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/sidebars/develop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ export default [
{
text: "develop.blocks.block-entity-renderer",
link: "/develop/blocks/block-entity-renderer",
},
{
text: "develop.blocks.inventory",
link: "/develop/blocks/inventory",
}
]
}
Expand Down
80 changes: 80 additions & 0 deletions develop/blocks/inventory.md
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.
Copy link
Contributor

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?


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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.
Now, when you right-click the block with an item, you'll no longer have it! If you run `/data get block` on the block, you'll see the item in the `Items` field in the NBT.


![Duplicator block & /data get block output showing the item in the inventory](/assets/develop/blocks/inventory_1.png)
Copy link
Member

Choose a reason for hiding this comment

The 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)
![Duplicator block and output of `/data get block` showing the item in the inventory](/assets/develop/blocks/inventory_1.png)


### 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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a ticking functionality here?
From what I can see, it's just copying the slot and is returning it asap.

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

View workflow job for this annotation

GitHub Actions / markdownlint

Custom rule

develop/blocks/inventory.md:58:1 search-replace Custom rule [missing-heading-anchor: Add anchors to headings. Use lowercase characters, numbers and dashes] [Context: "column: 1 text:'## Sided Inventories'"] https://vitepress.dev/guide/markdown#header-anchors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Sided Inventories
## Sided Inventories {#sided-inventories}


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" />
Binary file added public/assets/develop/blocks/inventory_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/assets/develop/blocks/inventory_2.mp4
Binary file not shown.
Binary file added public/assets/develop/blocks/inventory_3.webm
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public FabricDocsReferenceInternalModelProvider(FabricDataOutput output) {
public void generateBlockStateModels(BlockStateModelGenerator blockStateModelGenerator) {
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.CONDENSED_DIRT);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.COUNTER_BLOCK);
blockStateModelGenerator.registerSimpleCubeAll(ModBlocks.DUPLICATOR_BLOCK);

// TODO: This would be a good example for the model generation page. Move when needed.
// TODO: Actually make the model for the prismarine lamp - not sure how to do it via datagen.
Expand Down
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
Expand Up @@ -16,6 +16,7 @@

import com.example.docs.FabricDocsReference;
import com.example.docs.block.custom.CounterBlock;
import com.example.docs.block.custom.DuplicatorBlock;
import com.example.docs.block.custom.EngineBlock;
import com.example.docs.block.custom.PrismarineLampBlock;
import com.example.docs.item.ModItems;
Expand Down Expand Up @@ -84,6 +85,15 @@ public class ModBlocks {
);
// :::5

public static final RegistryKey<Block> DUPLICATOR_BLOCK_KEY = RegistryKey.of(
RegistryKeys.BLOCK,
Identifier.of(FabricDocsReference.MOD_ID, "duplicator")
);

public static final Block DUPLICATOR_BLOCK = register(
new DuplicatorBlock(AbstractBlock.Settings.create().registryKey(DUPLICATOR_BLOCK_KEY)), DUPLICATOR_BLOCK_KEY, true
);

// :::1
public static Block register(Block block, RegistryKey<Block> blockKey, boolean shouldRegisterItem) {
// Sometimes, you may not want to register an item for the block.
Expand Down Expand Up @@ -116,6 +126,7 @@ public static void setupItemGroups() {
itemGroup.add(ModBlocks.CONDENSED_OAK_LOG.asItem());
itemGroup.add(ModBlocks.PRISMARINE_LAMP.asItem());
itemGroup.add(ModBlocks.COUNTER_BLOCK.asItem());
itemGroup.add(ModBlocks.DUPLICATOR_BLOCK.asItem());
itemGroup.add(ModBlocks.ENGINE_BLOCK.asItem());
});
}
Expand Down
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
Expand Up @@ -12,12 +12,16 @@
import com.example.docs.FabricDocsReference;
import com.example.docs.block.ModBlocks;
import com.example.docs.block.entity.custom.CounterBlockEntity;
import com.example.docs.block.entity.custom.DuplicatorBlockEntity;
import com.example.docs.block.entity.custom.EngineBlockEntity;

public class ModBlockEntities {
public static final BlockEntityType<EngineBlockEntity> ENGINE_BLOCK_ENTITY =
register("engine", EngineBlockEntity::new, ModBlocks.ENGINE_BLOCK);

public static final BlockEntityType<DuplicatorBlockEntity> DUPLICATOR_BLOCK_ENTITY =
register("duplicator", DuplicatorBlockEntity::new, ModBlocks.DUPLICATOR_BLOCK);

// :::1
public static final BlockEntityType<CounterBlockEntity> COUNTER_BLOCK_ENTITY =
register("counter", CounterBlockEntity::new, ModBlocks.COUNTER_BLOCK);
Expand Down
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
Loading
Loading