====== 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.
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
public BlockEntity createBlockEntity(BlockView view) {
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) {
return Container.calculateComparatorOutput(world.getBlockEntity(pos));
}
}
Then we need to register our Block and BlockItem.
public class ExampleMod implements ModInitializer
{
public static final String MOD_ID = "tutorial";
// 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()
{
Registry.register(Registry.BLOCK, BIGGER_CHEST, BIGGER_CHEST_BLOCK);
Registry.register(Registry.ITEM, BIGGER_CHEST, new BlockItem(BIGGER_CHEST_BLOCK, new Item.Settings().group(ItemGroup.REDSTONE)));
}
}
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.
public class BiggerChestBlockEntity extends LootableContainerBlockEntity {
private DefaultedList 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 getInvStackList() {
return this.inventory;
}
@Override
protected void setInvStackList(DefaultedList 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;
}
}
==== 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.
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 {
// 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.
[...]
public static final String BIGGER_CHEST_TRANSLATION_KEY = Util.createTranslationKey("container", BIGGER_CHEST);
@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.registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY)));
}
==== Organizing ====
After all the steps, you should have your ExampleMod Class and ExampleClientMod Class as such:
public static final String MOD_ID = "tutorial";
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 final String BIGGER_CHEST_TRANSLATION_KEY = Util.createTranslationKey("container", BIGGER_CHEST);
public static BlockEntityType BIGGER_CHEST_ENTITY_TYPE;
@Override
public void onInitialize() {
Registry.register(Registry.BLOCK, BIGGER_CHEST, BIGGER_CHEST_BLOCK);
Registry.register(Registry.ITEM, BIGGER_CHEST, new BlockItem(BIGGER_CHEST_BLOCK, new Item.Settings().group(ItemGroup.REDSTONE)));
BIGGER_CHEST_ENTITY_TYPE = Registry.register(Registry.BLOCK_ENTITY_TYPE, BIGGER_CHEST, BlockEntityType.Builder.create(BiggerChestBlockEntity::new, BIGGER_CHEST_BLOCK).build(null));
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.registerFactory(ExampleMod.BIGGER_CHEST, (container) -> new BiggerChestScreen(container, MinecraftClient.getInstance().player.inventory, Text.translatable(ExampleMod.BIGGER_CHEST_TRANSLATION_KEY)));
}
{{tutorial:bigger_chest.png}}
It's over and enjoy your custom container!