We are going to make a bigger chest in this tutorial as an example.
First we need to create the Block and register it as well as its BlockItem.
public class BiggerChestBlock extends BlockWithEntity { public BiggerChestBlock(Settings settings) { super(settings); } // A side effect of extending BlockWithEntity is it changes the render type to INVISIBLE, so we have to revert this @Override public BlockRenderType getRenderType(BlockState state) { return BlockRenderType.MODEL; } // We will create the BlockEntity later. @Override return new BiggerChestBlockEntity(); } @Override public void onPlaced(World world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) { if (itemStack.hasCustomName()) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof BiggerChestBlockEntity) { ((BiggerChestBlockEntity)blockEntity).setCustomName(itemStack.getName()); } } } @Override public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { if (!world.isClient) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof BiggerChestBlockEntity) { ContainerProviderRegistry.INSTANCE.openContainer(ExampleMod.BIGGER_CHEST, player, buf -> buf.writeBlockPos(pos)); } } return ActionResult.SUCCESS; } // Scatter the items in the chest when it is removed. @Override public void onBlockRemoved(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if (state.getBlock() != newState.getBlock()) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof BiggerChestBlockEntity) { ItemScatterer.spawn(world, pos, (BiggerChestBlockEntity)blockEntity); // update comparators world.updateHorizontalAdjacent(pos, this); } super.onBlockRemoved(state, world, pos, newState, moved); } } @Override public boolean hasComparatorOutput(BlockState state) { return true; } @Override public int getComparatorOutput(BlockState state, World world, BlockPos pos) { } }
Then we need to register our Block and BlockItem.
public class ExampleMod implements ModInitializer { // a public identifier for multiple parts of our bigger chest public static final Identifier BIGGER_CHEST = new Identifier(MOD_ID, "bigger_chest_block"); public static final Block BIGGER_CHEST_BLOCK = new BiggerChestBlock(FabricBlockSettings.of(Material.METAL)); @Override public void onInitialize() { } }
You may refer to other tutorials to modify the appearance and other properties of the Block by adding models or adjusting rendering later.
BlockEntity is used for managing container inventories. Actually, it implements Inventory interface. It is required to save and load the inventory.
public class BiggerChestBlockEntity extends LootableContainerBlockEntity { private DefaultedList<ItemStack> inventory; private static final int INVENTORY_SIZE = 54; // 9 * 6 = 54 public BiggerChestBlockEntity() { super(ExampleMod.BIGGER_CHEST_ENTITY_TYPE); this.inventory = DefaultedList.ofSize(INVENTORY_SIZE, ItemStack.EMPTY); } @Override protected Text getContainerName() { // versions 1.18.2 and below return new TranslatableText("container.chest"); // versions since 1.19 return Text.translatable("container.chest"); } @Override protected ScreenHandler createScreenHandler(int syncId, PlayerInventory playerInventory) { return new BiggerChestScreenHandler(syncId, playerInventory, (Inventory) this); } @Override protected DefaultedList<ItemStack> getInvStackList() { return this.inventory; } @Override protected void setInvStackList(DefaultedList<ItemStack> list) { this.inventory = list; } @Override public int size() { return INVENTORY_SIZE; } @Override public void fromTag(CompoundTag tag) { super.fromTag(tag); this.inventory = DefaultedList.ofSize(this.size(), ItemStack.EMPTY); if (!this.deserializeLootTable(tag)) { Inventories.fromTag(tag, this.inventory); } } @Override public CompoundTag toTag(CompoundTag tag) { super.toTag(tag); if (!this.serializeLootTable(tag)) { Inventories.toTag(tag, this.inventory); } return tag; } }
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.
public class BiggerChestScreenHandler extends ScreenHandler { private final Inventory inventory; // Chest inventory private static final int INVENTORY_SIZE = 54; // 6 rows * 9 cols protected BiggerChestScreenHandler(int syncId, PlayerInventory playerInventory, Inventory inventory) { super(null, syncId); // Since we didn't create a ScreenHandlerType, we will place null here. this.inventory = inventory; checkSize(inventory, INVENTORY_SIZE); inventory.onOpen(playerInventory.player); // Creating Slots for GUI. A Slot is essentially a corresponding from inventory ItemStacks to the GUI position. int i; int j; // Chest Inventory for (i = 0; i < 6; i++) { for (j = 0; j < 9; j++) { this.addSlot(new Slot(inventory, i * 9 + j, 8 + j * 18, 18 + i * 18)); } } // Player Inventory (27 storage + 9 hotbar) for (i = 0; i < 3; i++) { for (j = 0; j < 9; j++) { this.addSlot(new Slot(playerInventory, i * 9 + j + 9, 8 + j * 18, 18 + i * 18 + 103 + 18)); } } for (j = 0; j < 9; j++) { this.addSlot(new Slot(playerInventory, j, 8 + j * 18, 18 + 161 + 18)); } } @Override public boolean canUse(PlayerEntity player) { return this.inventory.canPlayerUse(player); } // Shift + Player Inv Slot @Override public ItemStack transferSlot(PlayerEntity player, int invSlot) { ItemStack newStack = ItemStack.EMPTY; Slot slot = this.slots.get(invSlot); if (slot != null && slot.hasStack()) { ItemStack originalStack = slot.getStack(); newStack = originalStack.copy(); if (invSlot < this.inventory.getInvSize()) { if (!this.insertItem(originalStack, this.inventory.getInvSize(), this.slots.size(), true)) { return ItemStack.EMPTY; } } else if (!this.insertItem(originalStack, 0, this.inventory.getInvSize(), false)) { return ItemStack.EMPTY; } if (originalStack.isEmpty()) { slot.setStack(ItemStack.EMPTY); } else { slot.markDirty(); } } return newStack; } }
public class BiggerChestScreen extends HandledScreen<BiggerChestScreenHandler> { // a path to gui texture, you may replace it with new Identifier(YourMod.MOD_ID, "textures/gui/container/your_container.png"); private static final Identifier TEXTURE = new Identifier("textures/gui/container/generic_54.png"); public BiggerChestScreen(BiggerChestScreenHandler handler, PlayerInventory playerInventory, Text title) { super(handler, playerInventory, title); this.backgroundHeight = 114 + 6 * 18; } @Override protected void drawForeground(MatrixStack matrices, int mouseX, int mouseY) { this.textRenderer.draw(matrices, this.title.asString(), 8.0F, 6.0F, 4210752); this.textRenderer.draw(matrices, this.playerInventory.getDisplayName().asString(), 8.0F, (float)(this.backgroundHeight - 96 + 2), 4210752); } @Override protected void drawBackground(float delta, int mouseX, int mouseY) { RenderSystem.color4f(1.0F, 1.0F, 1.0F, 1.0F); this.client.getTextureManager().bindTexture(TEXTURE); int i = (this.width - this.backgroundWidth) / 2; int j = (this.height - this.backgroundHeight) / 2; this.blit(i, j, 0, 0, this.backgroundWidth, 6 * 18 + 17); this.blit(i, j + 6 * 18 + 17, 0, 126, this.backgroundWidth, 96); } }
Then you need to register them respectively on main initializers and client initializers.
[...] @Override public void onInitialize() { [...] ContainerProviderRegistry.INSTANCE.registerFactory(BIGGER_CHEST, (syncId, identifier, player, buf) -> { final World world = player.world; final BlockPos pos = buf.readBlockPos(); return world.getBlockState(pos).createContainerFactory(player.world, pos).createMenu(syncId, player.inventory, player); }); }
@Override public void onInitializeClient() { [...] ScreenProviderRegistry.INSTANCE.<BiggerChestContainer>registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY))); }
After all the steps, you should have your ExampleMod Class and ExampleClientMod Class as such:
public static final Identifier BIGGER_CHEST = new Identifier(MOD_ID, "bigger_chest"); public static final Block BIGGER_CHEST_BLOCK = new BiggerChestBlock(FabricBlockSettings.of(Material.WOOD).build()); public static BlockEntityType<BiggerChestBlockEntity> BIGGER_CHEST_ENTITY_TYPE; @Override public void onInitialize() { ContainerProviderRegistry.INSTANCE.registerFactory(BIGGER_CHEST, (syncId, identifier, player, buf) -> { final BlockEntity blockEntity = player.world.getBlockEntity(buf.readBlockPos()); return((BiggerChestBlockEntity) blockEntity).createContainer(syncId, player.inventory); }); }
@Override public void onInitializeClient() { ScreenProviderRegistry.INSTANCE.<BiggerChestContainer>registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY))); }
It's over and enjoy your custom container!