Block Entities and Menus
Add blockEntity(...) to a block definition, and each such block can have saved data, code that runs every tick, and an inventory.
Data and ticks
BlockDefinition.builder().blockEntity(BlockEntityDefinition.builder()
.inventory(3, "container.my_mod.crate") // 27 slots; using the block opens a chest menu
.ticker(entity -> entity.data().putInt("ticks", entity.data().getInt("ticks", 0) + 1))
.syncToClients()
.build());
world.blockEntityAt(pos).flatMap(BlockEntityView::inventory).ifPresent(inv -> inv.add(item, 10));
- Data:
entity.data()is aDataTree. It stores integers, longs, floating-point numbers, strings, booleans, byte arrays, and subtrees (child), and it is saved with the world. Callentity.markChanged()after you change it. - Ticks:
tickerruns every tick on the server. - Client sync: With
syncToClients()on, the data goes to the client onmarkChanged(). Turn it on if block entity drawing needs the data. BlockEntityView:world(),pos(),data(),inventory(),markChanged()
Chest-style inventory
inventory(rows, title) uses the game's chest menu (1 to 6 rows).
- It works as a game container. Hoppers and comparators work with it, and its contents drop when the block is broken.
InventoryView:size,get(slot),set(slot, item, count),add(item, count)(returns the count that did not fit),count(item),stackAt(slot),clear
Slot-layout menu and progress bar
For a menu that is not chest-shaped, place the slots yourself with MenuLayout.
BlockDefinition.builder()
.blockEntity(BlockEntityDefinition.builder()
.menu(MenuLayout.builder(176, 166)
.slot(56, 35).outputSlot(116, 35) // an input slot, and a take-only slot
.progressBar(79, 34, MenuLayout.ProgressStyle.ARROW, "progress", "total")
.build(), "container.my_mod.grinder")
.ticker(entity -> { /* every tick: entity.data(), entity.inventory() */ })
.syncToClients()
.build())
.build();
MenuLayout.Builder | Meaning |
|---|---|
slot(x, y) / outputSlot(x, y) | An input slot / a take-only slot |
grid(x, y, columns, rows) | A grid of slots |
playerInventory(x, y) | Position of the player inventory |
background(texture) | Background texture |
progressBar(x, y, style, valueKey, maxKey) | Progress bar (ARROW, FLAME) |
- The inventory size equals the number of slots in the layout. A menu type with the same name as the block ID is registered.
- If you give no background texture, Poly Mod Loader draws a gray panel with slot borders, similar to the game's.
- The progress bar is filled from two integer keys in the block entity data (value and maximum). They are sent to the client while the menu is open, and the range is
-32768..32767. - You cannot combine it with chest rows (
inventory(rows, …)).
Opening a menu
PlayerView.openMenu(pos) opens both mod menus and game menus (such as a furnace). Call it on the server thread.
player.openMenu(pos);
Example: grinder tick
The grinder in examples/native-showcase-mod turns one cobblestone into gravel every 40 ticks.
private static void grind(BlockEntityView entity) {
InventoryView inventory = entity.inventory().orElseThrow();
DataTree data = entity.data();
ItemStackView input = inventory.get(0);
ItemStackView output = inventory.get(1);
ResourceId gravel = ResourceId.minecraft("gravel");
boolean canGrind = input.item().equals(ResourceId.minecraft("cobblestone"))
&& (output.isEmpty() || (output.item().equals(gravel) && output.count() < 64));
if (!canGrind) {
return;
}
int progress = data.getInt("progress", 0) + 1;
if (progress >= 40) {
inventory.set(0, input.item(), input.count() - 1);
inventory.set(1, gravel, output.count() + 1);
progress = 0;
}
data.putInt("progress", progress).putInt("total", 40);
entity.markChanged();
}
Data versions
If you change the format of block entity data, convert the old data with dataVersion(version, migration). See Saved data and versions.