Workstations 26.1.2
Learn how to create a workstation.
PREREQUISITES
This workstation uses a custom recipe type, which can be found in Custom Recipe Types.
This tutorial will provide instructions on how to create a custom workstation. Unlike chests, workstations do not necessarily need to retain their inventory after the UI is closed (blocks such as the Crafting Table do not save their inventory, but other blocks, such as Furnaces, do). For demonstration purposes, we will not be using a BlockEntity.
Creating a Menu
INFO
For more details on creating menus, see Container Menus.
To allow us to craft our recipe in the GUI, we will create a block with a Menu. To open the menu, we will need to override some methods in our Block class:
java
@Override
protected InteractionResult useWithoutItem(BlockState blockState, Level level, BlockPos blockPos, Player player, BlockHitResult blockHitResult) {
if (!level.isClientSide()) {
player.openMenu(blockState.getMenuProvider(level, blockPos));
//player.awardStat(); (you can increment a custom stat here)
}
return InteractionResult.SUCCESS;
}
@Override
protected @Nullable MenuProvider getMenuProvider(BlockState state, Level level, BlockPos pos) {
return new SimpleMenuProvider(
(containerId, inventory, player) -> new UpgradingMenu(containerId, inventory, ContainerLevelAccess.create(level, pos)), this.getName()
);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
After that, we are ready to create the menu.
java
public class UpgradingMenu extends AbstractContainerMenu {
private final Container input = new SimpleContainer(2) {
@Override
public void setChanged() {
super.setChanged();
UpgradingMenu.this.slotsChanged(this);
}
};
private final ResultContainer output = new ResultContainer();
private final ContainerLevelAccess access;
@Nullable
private final Player player;
public UpgradingMenu(int containerId, Inventory inventory) {
this(containerId, inventory, ContainerLevelAccess.NULL);
}
public UpgradingMenu(int containerId, Inventory inventory, ContainerLevelAccess access) {
super(ModMenuTypes.UPGRADING_MENU_TYPE, containerId);
this.access = access;
this.player = inventory.player;
addSlot(new Slot(this.input, 0, 27, 47));
addSlot(new Slot(this.input, 1, 76, 47));
addSlot(new UpgradingResultSlot(this, this.output, 0, 134, 47));
addStandardInventorySlots(inventory, 8, 84);
}
/**
* Called by {@link UpgradingResultSlot#onTake(Player, ItemStack)}.
*/
protected void onTake(final Player player, final ItemStack stack) {
stack.onCraftedBy(player, stack.getCount());
this.output.awardUsedRecipes(player, List.of(this.input.getItem(0), this.input.getItem(1)));
this.input.removeItem(0, 1);
this.input.removeItem(1, 1);
}
@Override
public void slotsChanged(Container container) {
super.slotsChanged(container);
this.access.execute((level, blockPos) -> {
if (level instanceof ServerLevel serverLevel && container == this.input) {
UpgradingRecipeInput recipeInput = new UpgradingRecipeInput(this.input.getItem(0), this.input.getItem(1));
Optional<RecipeHolder<UpgradingRecipe>> maybeRecipe = serverLevel.recipeAccess().getRecipeFor(ExampleModRecipes.UPGRADING_RECIPE_TYPE, recipeInput, serverLevel);
ItemStack result = ItemStack.EMPTY;
if (maybeRecipe.isPresent()) {
RecipeHolder<UpgradingRecipe> recipeHolder = maybeRecipe.get();
UpgradingRecipe recipe = recipeHolder.value();
if (this.output.setRecipeUsed((ServerPlayer) this.player, recipeHolder)) {
ItemStack recipeResult = recipe.assemble(recipeInput);
if (recipeResult.isItemEnabled(level.enabledFeatures())) {
result = recipeResult;
}
}
} else {
// We can set the used recipe to null if no recipe was found.
//noinspection DataFlowIssue
this.output.setRecipeUsed((ServerPlayer) this.player, null);
}
this.output.setItem(0, result);
/*
Alternatively, call broadcastChanges instead of setting the remote slot and sending a packet.
Based on how your Menu is structured, you may not need to manually call any syncing method, but it is recommended that you are very sure of yourself before you remove these calls to avoid server-client desyncs.
*/
this.setRemoteSlot(0, result);
((ServerPlayer) this.player).connection.send(new ClientboundContainerSetSlotPacket(this.containerId, this.incrementStateId(), 0, result));
}
});
}
@Override
public ItemStack quickMoveStack(Player player, int slotIndex) {
return ItemStack.EMPTY;
}
@Override
public boolean stillValid(Player player) {
return stillValid(this.access, player, ModBlocks.UPGRADING_BLOCK);
}
@Override
public void removed(Player player) {
super.removed(player);
this.access.execute((level, blockPos) -> this.clearContainer(player, this.input));
}
@Override
public boolean canTakeItemForPickAll(final ItemStack carried, final Slot target) {
return target.container != this.output && super.canTakeItemForPickAll(carried, target);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
To go along with this menu, we'll also need a custom result Slot.
java
public class UpgradingResultSlot extends Slot {
private final UpgradingMenu menu;
public UpgradingResultSlot(UpgradingMenu menu, Container container, int slot, int x, int y) {
super(container, slot, x, y);
this.menu = menu;
}
@Override
public void onTake(Player player, ItemStack carried) {
this.menu.onTake(player, carried);
}
@Override
public boolean mayPlace(ItemStack itemStack) {
return false;
}
@Override
public boolean isFake() {
return true;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
A lot to unpack here! This menu has two input slots and an output UpgradingResultSlot.
The input container is an anonymous subclass of SimpleContainer, which calls the menu's slotsChanged method when its items change. In slotsChanged, we then create an instance of our recipe input class, filling it with the two input slots.
In order to see if it matches any recipes, we'll first ensure we are on the server level, since clients do not know what recipes exist. Then, we'll retrieve the RecipeManager via serverLevel.recipeAccess().
An Aside: Recipe Synchronization
If the client doesn't know what recipes exist, then how does the recipe book work?
I'm glad you asked. The server tells the client which recipes exist based on which recipes you have unlocked (done by completing certain criteria described in each recipe's advancement JSON, such as obtaining an item or entering water (for boats)). However, this is fairly annoying for recipe viewer mods, which would ideally like to see all recipes available, but can now only see the recipes that the client gets from the server. To bypass this, we can use Fabric API to synchronize our recipes.
We'll call serverLevel.recipeAccess().getRecipeFor with our recipe input to get a recipe that matches the inputs. If a recipe was found, we can add or remove the result from the result container.
To detect when the user takes the result out, we use the UpgradingResultSlot's onTake override. The onTake method of our menu then decrements the input items.
To ensure that the player is within interaction range from the block, we override stillValid.
WARNING
Ensure the Block you pass as an argument for stillValid is the block opening the menu! If you don't, the menu and screen might open and proceed to close themselves immediately.
Finally, to prevent deleting items, it is important to drop the inputs back when the screen is closed, as shown in the removed method.
INFO
You may have noticed that multiple methods contain a ContainerLevelAccess#execute call. This a wrapper class used by Mojang to ensure the correct Level and position are in use when interactions occur, and prevents players from accessing containers they shouldn't be accessing. Note that the special NULL ContainerLevelAccess performs no action when execute is called on it.
The mayPlace method of the Slot returns false so players cannot insert items into the result slot, and the isFake method tells the Screen that the stack it contains does not have an owner (yet).
You also need to add the menu to the registry:
java
public static final MenuType<UpgradingMenu> UPGRADING_MENU_TYPE = register("upgrading", UpgradingMenu::new);1
Finally, we need to register our block:
java
public static final Block UPGRADING_BLOCK = register(
"upgrading_block", UpgradingBlock::new, BlockBehaviour.Properties.of(), true
);1
2
3
2
3
Implementing quickMoveStack
INFO
See also: Container Menus: Creating the Menu
Quick Move is called whenever a shift-click is performed in a Menu.
java
private static final int INPUT_SLOTS_COUNT = 2;
private static final int RESULT_SLOT = 0;
private static final int INPUT_SLOTS_START = RESULT_SLOT + 1; // 1
private static final int INPUT_SLOTS_END = INPUT_SLOTS_START + INPUT_SLOTS_COUNT; // 3
private static final int INVENTORY_START = INPUT_SLOTS_END; // 3
private static final int INVENTORY_END = INVENTORY_START + 27; // 30
private static final int HOTBAR_START = INVENTORY_END; // 30
private static final int HOTBAR_END = HOTBAR_START + 9; // 39
@Override
public ItemStack quickMoveStack(Player player, int slotIndex) {
Slot slot = this.slots.get(slotIndex);
//noinspection ConstantValue
if (slot == null || !slot.hasItem()) {
return ItemStack.EMPTY;
}
ItemStack stack = slot.getItem();
ItemStack clicked = stack.copy();
if (slotIndex == RESULT_SLOT) {
stack.getItem().onCraftedBy(stack, player);
if (!this.moveItemStackTo(stack, INVENTORY_START, HOTBAR_END, true)) {
return ItemStack.EMPTY;
}
slot.onQuickCraft(stack, clicked);
} else if (slotIndex >= INVENTORY_START && slotIndex < HOTBAR_END) {
if (!this.moveItemStackTo(stack, INPUT_SLOTS_START, INPUT_SLOTS_END, false)) {
if (slotIndex < HOTBAR_START) {
if (!this.moveItemStackTo(stack, HOTBAR_START, HOTBAR_END, false)) {
return ItemStack.EMPTY;
}
} else if (!this.moveItemStackTo(stack, INVENTORY_START, INVENTORY_END, false)) {
return ItemStack.EMPTY;
}
}
} else if (!this.moveItemStackTo(stack, INVENTORY_START, HOTBAR_END, false)) {
return ItemStack.EMPTY;
}
if (stack.isEmpty()) {
slot.setByPlayer(ItemStack.EMPTY);
} else {
slot.setChanged();
}
if (stack.getCount() == clicked.getCount()) {
return ItemStack.EMPTY;
}
slot.onTake(player, stack);
if (slotIndex == RESULT_SLOT) {
player.drop(stack, false);
}
return clicked;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Wow, that's a lot of code again. Let's try thinking through what's happening.
Usually, when quick-moving a stack from the inventory area, the menu first checks if the clicked slot is the result slot (with index 0). If so, the menu tries to move the result stack into the inventory, but if that fails, nothing happens.
Next, the menu checks to see if the slot clicked belongs to the inventory. If so, then the menu tries to move the stack into the inputs. If that failed, we try to move the stack within the inventory (slots clicked in the hotbar move their stacks into the other 27 slots of the inventory, and vice-versa).
If the clicked slot was not the result slot or within the inventory, the slot is then almost guaranteed to have been one of our two input slots, so we would want to move their stack back into the inventory.
The Screen
INFO
See also: Container Menus: Creating the Screen
For now, we can just borrow the vanilla Anvil's background texture.
java
public class UpgradingScreen extends AbstractContainerScreen<UpgradingMenu> {
private static final Identifier SCREEN_TEXTURE = Identifier.withDefaultNamespace("textures/gui/container/anvil.png");
public UpgradingScreen(UpgradingMenu abstractContainerMenu, Inventory inventory, Component component) {
super(abstractContainerMenu, inventory, component);
}
@Override
public void extractBackground(GuiGraphicsExtractor guiGraphics, int mouseX, int mouseY, float delta) {
guiGraphics.blit(RenderPipelines.GUI_TEXTURED, SCREEN_TEXTURE, this.leftPos, this.topPos, 0.0F, 0.0F, this.imageWidth, this.imageHeight, 256, 256);
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
Don't forget to bind your menu type to the screen in your ClientModInitializer, like so:
java
MenuScreens.register(ModMenuTypes.UPGRADING_MENU_TYPE, UpgradingScreen::new);1
Recipe Remainders
Want to make a recipe that supports remainders? We recommend taking a look at net.minecraft.world.inventory.ResultSlot#getRemainingItems. The crafting table uses this as its result slot, so many similarities to the docs can be found, but there are also some differences.



