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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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.

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.

![Duplicator block & /data get block output 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`.

<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

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 @@ -14,6 +14,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 @@ -56,6 +57,10 @@ public class ModBlocks {
);
// :::5

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

// :::1
public static Block register(Block block, String name, boolean shouldRegisterItem) {
// Register the block and its item.
Expand Down Expand Up @@ -83,6 +88,7 @@ public static void initialize() {
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());
});
}

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.Hand;
import net.minecraft.util.ItemActionResult;
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 ItemActionResult onUseWithItem(ItemStack stack, BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (!(world.getBlockEntity(pos) instanceof DuplicatorBlockEntity duplicatorBlockEntity)) {
return ItemActionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}

// :::2

// :::3
if (!duplicatorBlockEntity.canInsert(0, stack, hit.getSide())) {
return ItemActionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}

// :::3

// :::2
if (!player.getStackInHand(hand).isEmpty() && duplicatorBlockEntity.isEmpty()) {
duplicatorBlockEntity.setStack(0, player.getStackInHand(hand).copy());
player.getStackInHand(hand).setCount(0);
}

return ItemActionResult.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 @@ -10,12 +10,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