diff --git a/.vitepress/sidebars/develop.ts b/.vitepress/sidebars/develop.ts
index fbe2f1525..aece122a2 100644
--- a/.vitepress/sidebars/develop.ts
+++ b/.vitepress/sidebars/develop.ts
@@ -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",
}
]
}
diff --git a/develop/blocks/inventory.md b/develop/blocks/inventory.md
new file mode 100644
index 000000000..9d8a9de61
--- /dev/null
+++ b/develop/blocks/inventory.md
@@ -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`.
+
+
+
+## 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!
+
+
diff --git a/public/assets/develop/blocks/inventory_1.png b/public/assets/develop/blocks/inventory_1.png
new file mode 100644
index 000000000..4d04d7c4f
Binary files /dev/null and b/public/assets/develop/blocks/inventory_1.png differ
diff --git a/public/assets/develop/blocks/inventory_2.mp4 b/public/assets/develop/blocks/inventory_2.mp4
new file mode 100644
index 000000000..4fafcf3de
Binary files /dev/null and b/public/assets/develop/blocks/inventory_2.mp4 differ
diff --git a/public/assets/develop/blocks/inventory_3.webm b/public/assets/develop/blocks/inventory_3.webm
new file mode 100644
index 000000000..357799605
Binary files /dev/null and b/public/assets/develop/blocks/inventory_3.webm differ
diff --git a/reference/latest/src/client/java/com/example/docs/datagen/internal/FabricDocsReferenceInternalModelProvider.java b/reference/latest/src/client/java/com/example/docs/datagen/internal/FabricDocsReferenceInternalModelProvider.java
index 902876d83..d8389df6c 100644
--- a/reference/latest/src/client/java/com/example/docs/datagen/internal/FabricDocsReferenceInternalModelProvider.java
+++ b/reference/latest/src/client/java/com/example/docs/datagen/internal/FabricDocsReferenceInternalModelProvider.java
@@ -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.
diff --git a/reference/latest/src/main/generated/assets/fabric-docs-reference/blockstates/duplicator.json b/reference/latest/src/main/generated/assets/fabric-docs-reference/blockstates/duplicator.json
new file mode 100644
index 000000000..7698f83ae
--- /dev/null
+++ b/reference/latest/src/main/generated/assets/fabric-docs-reference/blockstates/duplicator.json
@@ -0,0 +1,7 @@
+{
+ "variants": {
+ "": {
+ "model": "fabric-docs-reference:block/duplicator"
+ }
+ }
+}
\ No newline at end of file
diff --git a/reference/latest/src/main/generated/assets/fabric-docs-reference/items/duplicator.json b/reference/latest/src/main/generated/assets/fabric-docs-reference/items/duplicator.json
new file mode 100644
index 000000000..837f17327
--- /dev/null
+++ b/reference/latest/src/main/generated/assets/fabric-docs-reference/items/duplicator.json
@@ -0,0 +1,6 @@
+{
+ "model": {
+ "type": "minecraft:model",
+ "model": "fabric-docs-reference:block/duplicator"
+ }
+}
\ No newline at end of file
diff --git a/reference/latest/src/main/generated/assets/fabric-docs-reference/models/block/duplicator.json b/reference/latest/src/main/generated/assets/fabric-docs-reference/models/block/duplicator.json
new file mode 100644
index 000000000..a6a040f25
--- /dev/null
+++ b/reference/latest/src/main/generated/assets/fabric-docs-reference/models/block/duplicator.json
@@ -0,0 +1,6 @@
+{
+ "parent": "minecraft:block/cube_all",
+ "textures": {
+ "all": "fabric-docs-reference:block/duplicator"
+ }
+}
\ No newline at end of file
diff --git a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
index d503a9bb8..fc6168dd5 100644
--- a/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
+++ b/reference/latest/src/main/java/com/example/docs/block/ModBlocks.java
@@ -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;
@@ -84,6 +85,15 @@ public class ModBlocks {
);
// :::5
+ public static final RegistryKey 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 blockKey, boolean shouldRegisterItem) {
// Sometimes, you may not want to register an item for the block.
@@ -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());
});
}
diff --git a/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java b/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java
new file mode 100644
index 000000000..09956913d
--- /dev/null
+++ b/reference/latest/src/main/java/com/example/docs/block/custom/DuplicatorBlock.java
@@ -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 BlockEntityTicker getTicker(World world, BlockState state, BlockEntityType type) {
+ return validateTicker(type, ModBlockEntities.DUPLICATOR_BLOCK_ENTITY, DuplicatorBlockEntity::tick);
+ }
+
+ // :::1
+ // ...
+}
+// :::1
diff --git a/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java b/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java
index 775e32996..97df78c24 100644
--- a/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java
+++ b/reference/latest/src/main/java/com/example/docs/block/entity/ModBlockEntities.java
@@ -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 ENGINE_BLOCK_ENTITY =
register("engine", EngineBlockEntity::new, ModBlocks.ENGINE_BLOCK);
+ public static final BlockEntityType DUPLICATOR_BLOCK_ENTITY =
+ register("duplicator", DuplicatorBlockEntity::new, ModBlocks.DUPLICATOR_BLOCK);
+
// :::1
public static final BlockEntityType COUNTER_BLOCK_ENTITY =
register("counter", CounterBlockEntity::new, ModBlocks.COUNTER_BLOCK);
diff --git a/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java b/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java
new file mode 100644
index 000000000..0f656b12b
--- /dev/null
+++ b/reference/latest/src/main/java/com/example/docs/block/entity/custom/DuplicatorBlockEntity.java
@@ -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 items = DefaultedList.ofSize(1, ItemStack.EMPTY);
+
+ public DuplicatorBlockEntity(BlockPos pos, BlockState state) {
+ super(ModBlockEntities.DUPLICATOR_BLOCK_ENTITY, pos, state);
+ }
+
+ @Override
+ public DefaultedList 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
diff --git a/reference/latest/src/main/java/com/example/docs/inventory/ImplementedInventory.java b/reference/latest/src/main/java/com/example/docs/inventory/ImplementedInventory.java
new file mode 100644
index 000000000..d287bffaa
--- /dev/null
+++ b/reference/latest/src/main/java/com/example/docs/inventory/ImplementedInventory.java
@@ -0,0 +1,135 @@
+package com.example.docs.inventory;
+
+import net.minecraft.entity.player.PlayerEntity;
+import net.minecraft.inventory.Inventories;
+import net.minecraft.inventory.Inventory;
+import net.minecraft.item.ItemStack;
+import net.minecraft.util.collection.DefaultedList;
+
+/**
+ * A simple {@code Inventory} implementation with only default methods + an item list getter.
+ *
+ * @author Juuz
+ */
+public interface ImplementedInventory extends Inventory {
+ /**
+ * Retrieves the item list of this inventory.
+ * Must return the same instance every time it's called.
+ */
+ DefaultedList getItems();
+
+ /**
+ * Creates an inventory from the item list.
+ */
+ static ImplementedInventory of(DefaultedList items) {
+ return () -> items;
+ }
+
+ /**
+ * Creates a new inventory with the specified size.
+ */
+ static ImplementedInventory ofSize(int size) {
+ return of(DefaultedList.ofSize(size, ItemStack.EMPTY));
+ }
+
+ /**
+ * Returns the inventory size.
+ */
+ @Override
+ default int size() {
+ return getItems().size();
+ }
+
+ /**
+ * Checks if the inventory is empty.
+ * @return true if this inventory has only empty stacks, false otherwise.
+ */
+ @Override
+ default boolean isEmpty() {
+ for (int i = 0; i < size(); i++) {
+ ItemStack stack = getStack(i);
+
+ if (!stack.isEmpty()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Retrieves the item in the slot.
+ */
+ @Override
+ default ItemStack getStack(int slot) {
+ return getItems().get(slot);
+ }
+
+ /**
+ * Removes items from an inventory slot.
+ * @param slot The slot to remove from.
+ * @param count How many items to remove. If there are less items in the slot than what are requested,
+ * takes all items in that slot.
+ */
+ @Override
+ default ItemStack removeStack(int slot, int count) {
+ ItemStack result = Inventories.splitStack(getItems(), slot, count);
+
+ if (!result.isEmpty()) {
+ markDirty();
+ }
+
+ return result;
+ }
+
+ /**
+ * Removes all items from an inventory slot.
+ * @param slot The slot to remove from.
+ */
+ @Override
+ default ItemStack removeStack(int slot) {
+ return Inventories.removeStack(getItems(), slot);
+ }
+
+ /**
+ * Replaces the current stack in an inventory slot with the provided stack.
+ * @param slot The inventory slot of which to replace the itemstack.
+ * @param stack The replacing itemstack. If the stack is too big for
+ * this inventory ({@link Inventory#getMaxCountPerStack()}),
+ * it gets resized to this inventory's maximum amount.
+ */
+ @Override
+ default void setStack(int slot, ItemStack stack) {
+ getItems().set(slot, stack);
+
+ if (stack.getCount() > stack.getMaxCount()) {
+ stack.setCount(stack.getMaxCount());
+ }
+ }
+
+ /**
+ * Clears the inventory.
+ */
+ @Override
+ default void clear() {
+ getItems().clear();
+ }
+
+ /**
+ * Marks the state as dirty.
+ * Must be called after changes in the inventory, so that the game can properly save
+ * the inventory contents and notify neighboring blocks of inventory changes.
+ */
+ @Override
+ default void markDirty() {
+ // Override if you want behavior.
+ }
+
+ /**
+ * @return true if the player can use the inventory, false otherwise.
+ */
+ @Override
+ default boolean canPlayerUse(PlayerEntity player) {
+ return true;
+ }
+}
diff --git a/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/duplicator.png b/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/duplicator.png
new file mode 100644
index 000000000..5c0c440d9
Binary files /dev/null and b/reference/latest/src/main/resources/assets/fabric-docs-reference/textures/block/duplicator.png differ
diff --git a/sidebar_translations.json b/sidebar_translations.json
index 23dd41fc7..92c15d45b 100644
--- a/sidebar_translations.json
+++ b/sidebar_translations.json
@@ -35,6 +35,7 @@
"develop.blocks.blockstates": "Block States",
"develop.blocks.block-entities": "Block Entities",
"develop.blocks.block-entity-renderer": "Block Entity Renderers",
+ "develop.blocks.inventory": "Block Inventories",
"develop.entities": "Entities",
"develop.entities.effects": "Status Effects",
"develop.entities.damage-types": "Damage Types",