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 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",