Table of Contents

Creating a Container Block (DRAFT)

We are going to make a bigger chest in this tutorial as an example.

Block and BlockItem

First we need to create the Block and register it as well as its BlockItem.

BiggerChestBlock.java
  1. public class BiggerChestBlock extends BlockWithEntity {
  2. public BiggerChestBlock(Settings settings) {
  3. super(settings);
  4. }
  5.  
  6. // A side effect of extending BlockWithEntity is it changes the render type to INVISIBLE, so we have to revert this
  7. @Override
  8. public BlockRenderType getRenderType(BlockState state) {
  9. return BlockRenderType.MODEL;
  10. }
  11.  
  12. // We will create the BlockEntity later.
  13. @Override
  14. public BlockEntity createBlockEntity(BlockView view) {
  15. return new BiggerChestBlockEntity();
  16. }
  17.  
  18. @Override
  19. public void onPlaced(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) {
  20. if (itemStack.hasCustomName()) {
  21. BlockEntity blockEntity = world.getBlockEntity(pos);
  22. if (blockEntity instanceof BiggerChestBlockEntity) {
  23. ((BiggerChestBlockEntity)blockEntity).setCustomName(itemStack.getName());
  24. }
  25. }
  26. }
  27.  
  28. @Override
  29. public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
  30. if (!world.isClient) {
  31. BlockEntity blockEntity = world.getBlockEntity(pos);
  32. if (blockEntity instanceof BiggerChestBlockEntity) {
  33. ContainerProviderRegistry.INSTANCE.openContainer(ExampleMod.BIGGER_CHEST, player, buf -> buf.writeBlockPos(pos));
  34. }
  35. }
  36. return ActionResult.SUCCESS;
  37. }
  38.  
  39. // Scatter the items in the chest when it is removed.
  40. @Override
  41. public void onBlockRemoved(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
  42. if (state.getBlock() != newState.getBlock()) {
  43. BlockEntity blockEntity = world.getBlockEntity(pos);
  44. if (blockEntity instanceof BiggerChestBlockEntity) {
  45. ItemScatterer.spawn(world, pos, (BiggerChestBlockEntity)blockEntity);
  46. // update comparators
  47. world.updateHorizontalAdjacent(pos, this);
  48. }
  49. super.onBlockRemoved(state, world, pos, newState, moved);
  50. }
  51. }
  52.  
  53. @Override
  54. public boolean hasComparatorOutput(BlockState state) {
  55. return true;
  56. }
  57.  
  58. @Override
  59. public int getComparatorOutput(BlockState state, World world, BlockPos pos) {
  60. return Container.calculateComparatorOutput(world.getBlockEntity(pos));
  61. }
  62. }

Then we need to register our Block and BlockItem.

ExampleMod.java
  1. public class ExampleMod implements ModInitializer
  2. {
  3.  
  4. public static final String MOD_ID = "tutorial";
  5.  
  6. // a public identifier for multiple parts of our bigger chest
  7. public static final Identifier BIGGER_CHEST = new Identifier(MOD_ID, "bigger_chest_block");
  8.  
  9. public static final Block BIGGER_CHEST_BLOCK = new BiggerChestBlock(FabricBlockSettings.of(Material.METAL));
  10.  
  11. @Override
  12. public void onInitialize()
  13. {
  14. Registry.register(Registry.BLOCK, BIGGER_CHEST, BIGGER_CHEST_BLOCK);
  15. Registry.register(Registry.ITEM, BIGGER_CHEST, new BlockItem(BIGGER_CHEST_BLOCK, new Item.Settings().group(ItemGroup.REDSTONE)));
  16. }
  17. }

You may refer to other tutorials to modify the appearance and other properties of the Block by adding models or adjusting rendering later.

BlockEntity

BlockEntity is used for managing container inventories. Actually, it implements Inventory interface. It is required to save and load the inventory.

BiggerChestBlockEntity.java
  1. public class BiggerChestBlockEntity extends LootableContainerBlockEntity {
  2. private DefaultedList<ItemStack> inventory;
  3. private static final int INVENTORY_SIZE = 54; // 9 * 6 = 54
  4.  
  5. public BiggerChestBlockEntity() {
  6. super(ExampleMod.BIGGER_CHEST_ENTITY_TYPE);
  7. this.inventory = DefaultedList.ofSize(INVENTORY_SIZE, ItemStack.EMPTY);
  8. }
  9.  
  10. @Override
  11. protected Text getContainerName() {
  12. // versions 1.18.2 and below
  13. return new TranslatableText("container.chest");
  14. // versions since 1.19
  15. return Text.translatable("container.chest");
  16. }
  17.  
  18. @Override
  19. protected ScreenHandler createScreenHandler(int syncId, PlayerInventory playerInventory) {
  20. return new BiggerChestScreenHandler(syncId, playerInventory, (Inventory) this);
  21. }
  22.  
  23. @Override
  24. protected DefaultedList<ItemStack> getInvStackList() {
  25. return this.inventory;
  26. }
  27.  
  28. @Override
  29. protected void setInvStackList(DefaultedList<ItemStack> list) {
  30. this.inventory = list;
  31. }
  32.  
  33. @Override
  34. public int size() {
  35. return INVENTORY_SIZE;
  36. }
  37.  
  38. @Override
  39. public void fromTag(CompoundTag tag) {
  40. super.fromTag(tag);
  41. this.inventory = DefaultedList.ofSize(this.size(), ItemStack.EMPTY);
  42. if (!this.deserializeLootTable(tag)) {
  43. Inventories.fromTag(tag, this.inventory);
  44. }
  45. }
  46.  
  47. @Override
  48. public CompoundTag toTag(CompoundTag tag) {
  49. super.toTag(tag);
  50. if (!this.serializeLootTable(tag)) {
  51. Inventories.toTag(tag, this.inventory);
  52. }
  53. return tag;
  54. }
  55. }

Container GUI and Screen

We need a ScreenHandler Class and a HandledScreen Class to display and sync the GUI. ScreenHandler classes are used to synchronize GUI state between the server and the client. HandledScreen classes are fully client-sided and are responsible for drawing GUI elements.

BiggerChestScreenHandler.java
  1. public class BiggerChestScreenHandler extends ScreenHandler {
  2. private final Inventory inventory; // Chest inventory
  3. private static final int INVENTORY_SIZE = 54; // 6 rows * 9 cols
  4.  
  5. protected BiggerChestScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory) {
  6. super(null, syncId); // Since we didn't create a ScreenHandlerType, we will place null here.
  7. this.inventory = inventory;
  8. checkSize(inventory, INVENTORY_SIZE);
  9. inventory.onOpen(playerInventory.player);
  10.  
  11. // Creating Slots for GUI. A Slot is essentially a corresponding from inventory ItemStacks to the GUI position.
  12. int i;
  13. int j;
  14.  
  15. // Chest Inventory
  16. for (i = 0; i < 6; i++) {
  17. for (j = 0; j < 9; j++) {
  18. this.addSlot(new Slot(inventory, i * 9 + j, 8 + j * 18, 18 + i * 18));
  19. }
  20. }
  21.  
  22.  
  23. // Player Inventory (27 storage + 9 hotbar)
  24. for (i = 0; i < 3; i++) {
  25. for (j = 0; j < 9; j++) {
  26. this.addSlot(new Slot(playerInventory, i * 9 + j + 9, 8 + j * 18, 18 + i * 18 + 103 + 18));
  27. }
  28. }
  29.  
  30.  
  31. for (j = 0; j < 9; j++) {
  32. this.addSlot(new Slot(playerInventory, j, 8 + j * 18, 18 + 161 + 18));
  33. }
  34. }
  35.  
  36. @Override
  37. public boolean canUse(PlayerEntity player) {
  38. return this.inventory.canPlayerUse(player);
  39. }
  40.  
  41. // Shift + Player Inv Slot
  42. @Override
  43. public ItemStack transferSlot(PlayerEntity player, int invSlot) {
  44. ItemStack newStack = ItemStack.EMPTY;
  45. Slot slot = this.slots.get(invSlot);
  46. if (slot != null && slot.hasStack()) {
  47. ItemStack originalStack = slot.getStack();
  48. newStack = originalStack.copy();
  49. if (invSlot < this.inventory.getInvSize()) {
  50. if (!this.insertItem(originalStack, this.inventory.getInvSize(), this.slots.size(), true)) {
  51. return ItemStack.EMPTY;
  52. }
  53. } else if (!this.insertItem(originalStack, 0, this.inventory.getInvSize(), false)) {
  54. return ItemStack.EMPTY;
  55. }
  56.  
  57. if (originalStack.isEmpty()) {
  58. slot.setStack(ItemStack.EMPTY);
  59. } else {
  60. slot.markDirty();
  61. }
  62. }
  63.  
  64. return newStack;
  65. }
  66. }
BiggerChestScreen.java
  1. public class BiggerChestScreen extends HandledScreen<BiggerChestScreenHandler> {
  2.  
  3. // a path to gui texture, you may replace it with new Identifier(YourMod.MOD_ID, "textures/gui/container/your_container.png");
  4. private static final Identifier TEXTURE = new Identifier("textures/gui/container/generic_54.png");
  5.  
  6. public BiggerChestScreen(BiggerChestScreenHandler handler, PlayerInventory playerInventory, Text title) {
  7. super(handler, playerInventory, title);
  8. this.backgroundHeight = 114 + 6 * 18;
  9. }
  10.  
  11. @Override
  12. protected void drawForeground(MatrixStack matrices, int mouseX, int mouseY) {
  13. this.textRenderer.draw(matrices, this.title.asString(), 8.0F, 6.0F, 4210752);
  14. this.textRenderer.draw(matrices, this.playerInventory.getDisplayName().asString(), 8.0F, (float)(this.backgroundHeight - 96 + 2), 4210752);
  15. }
  16.  
  17. @Override
  18. protected void drawBackground(float delta, int mouseX, int mouseY) {
  19. RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F);
  20. this.client.getTextureManager().bindTexture(TEXTURE);
  21. int i = (this.width - this.backgroundWidth) / 2;
  22. int j = (this.height - this.backgroundHeight) / 2;
  23. this.blit(i, j, 0, 0, this.backgroundWidth, 6 * 18 + 17);
  24. this.blit(i, j + 6 * 18 + 17, 0, 126, this.backgroundWidth, 96);
  25. }
  26. }

Then you need to register them respectively on main initializers and client initializers.

  1. [...]
  2. public static final String BIGGER_CHEST_TRANSLATION_KEY = Util.createTranslationKey("container", BIGGER_CHEST);
  3.  
  4. @Override
  5. public void onInitialize() {
  6. [...]
  7. ContainerProviderRegistry.INSTANCE.registerFactory(BIGGER_CHEST, (syncId, identifier, player, buf) -> {
  8. final World world = player.world;
  9. final BlockPos pos = buf.readBlockPos();
  10. return world.getBlockState(pos).createContainerFactory(player.world, pos).createMenu(syncId, player.inventory, player);
  11. });
  12. }
  1. @Override
  2. public void onInitializeClient() {
  3. [...]
  4. ScreenProviderRegistry.INSTANCE.<BiggerChestContainer>registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY)));
  5. }

Organizing

After all the steps, you should have your ExampleMod Class and ExampleClientMod Class as such:

ExampleMod.java
  1. public static final String MOD_ID = "tutorial";
  2.  
  3. public static final Identifier BIGGER_CHEST = new Identifier(MOD_ID, "bigger_chest");
  4. public static final Block BIGGER_CHEST_BLOCK = new BiggerChestBlock(FabricBlockSettings.of(Material.WOOD).build());
  5. public static final String BIGGER_CHEST_TRANSLATION_KEY = Util.createTranslationKey("container", BIGGER_CHEST);
  6.  
  7. public static BlockEntityType<BiggerChestBlockEntity> BIGGER_CHEST_ENTITY_TYPE;
  8.  
  9. @Override
  10. public void onInitialize() {
  11. Registry.register(Registry.BLOCK, BIGGER_CHEST, BIGGER_CHEST_BLOCK);
  12. Registry.register(Registry.ITEM, BIGGER_CHEST, new BlockItem(BIGGER_CHEST_BLOCK, new Item.Settings().group(ItemGroup.REDSTONE)));
  13. BIGGER_CHEST_ENTITY_TYPE = Registry.register(Registry.BLOCK_ENTITY_TYPE, BIGGER_CHEST, BlockEntityType.Builder.create(BiggerChestBlockEntity::new, BIGGER_CHEST_BLOCK).build(null));
  14. ContainerProviderRegistry.INSTANCE.registerFactory(BIGGER_CHEST, (syncId, identifier, player, buf) -> {
  15. final BlockEntity blockEntity = player.world.getBlockEntity(buf.readBlockPos());
  16. return((BiggerChestBlockEntity) blockEntity).createContainer(syncId, player.inventory);
  17. });
  18. }
ExampleClientMod.java
  1. @Override
  2. public void onInitializeClient() {
  3. ScreenProviderRegistry.INSTANCE.<BiggerChestContainer>registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY)));
  4. }

It's over and enjoy your custom container!