工作站 26.2
学习怎么去创建一个工作站。
前置知识
这个工作站使用了一种自定义配方类型,具体可以参考自定义配方类型。
本教程将指导你如何创建自定义工作站。 与箱子不同,工作站不一定需要在UI关闭后保留其物品栏(例如工作台之类的方块不会保存其物品栏,但其他方块,如熔炉则会保存)。 出于演示目的,我们这里将不使用方块实体。
创建菜单
INFO
有关创建菜单的更多详细信息,请参阅容器菜单。
为了允许我们在图形界面(GUI)中创建配方,我们将创建一个带有菜单的方块。 要打开菜单,我们需要重写 Block 类中的一些方法:
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
之后,我们就可以开始创建菜单了。
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;
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;
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);
if (container != this.input) {
return;
}
this.access.execute((level, pos) -> {
if (level instanceof ServerLevel serverLevel) {
this.inputsChanged(serverLevel);
}
});
}
public void inputsChanged(ServerLevel level) {
UpgradingRecipeInput recipeInput = new UpgradingRecipeInput(this.input.getItem(0), this.input.getItem(1));
Optional<RecipeHolder<UpgradingRecipe>> maybeRecipe = level.recipeAccess().getRecipeFor(ExampleModRecipes.UPGRADING_RECIPE_TYPE, recipeInput, level);
if (maybeRecipe.isPresent()) {
RecipeHolder<UpgradingRecipe> recipeHolder = maybeRecipe.get();
UpgradingRecipe recipe = recipeHolder.value();
this.output.setRecipeUsed(recipeHolder);
this.output.setItem(0, recipe.assemble(recipeInput));
} else {
// We can set the used recipe to null if no recipe was found.
this.output.setRecipeUsed(null);
this.output.setItem(0, ItemStack.EMPTY);
}
}
@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
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
为了配合这个菜单,我们还需要一个自定义的输出结果槽位 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
这里的信息量很大! 这个菜单包含两个输入槽位和一个输出槽位 UpgradingResultSlot。
输入容器是 SimpleContainer 的一个匿名子类,当其物品发生变化时,它会调用菜单的 slotsChanged 方法。 在 slotsChanged 中,我们创建一个配方输入类的实例,并用两个输入槽位填充它。
为了查看它是否匹配任何配方,我们首先要确保我们位于服务器级别,因为客户端不知道存在哪些配方。 然后,我们将通过 serverLevel.recipeAccess() 获取 RecipeManager。
补充说明:配方同步
如果客户端不知道存在哪些配方,那么配方书是如何工作的?
很高兴你问了这个问题。 服务器会根据你解锁了哪些配方来告知客户端存在哪些配方(解锁通过完成每个配方的进度 JSON 中描述的特定条件来实现,例如获得某个物品或进入水中(对于船而言))。 然而,对于配方查看类模组来说这相当令人头疼。它们理想情况下希望能看到所有可用的配方,但现在只能看到客户端从服务器获取到的配方。 为了绕过这一限制,我们可以使用 Fabric API 来同步我们的配方。
我们将调用 serverLevel.recipeAccess().getRecipeFor 并传入我们的配方输入,以获取与输入匹配的配方。 如果找到了配方,我们可以将结果添加到结果容器中或从中移除结果。
为了检测玩家何时取出了输出结果,我们重写了 UpgradingResultSlot 的 onTake 方法。 我们菜单的 onTake 方法随后会减少输入物品的数量。
为了确保玩家处于与方块互动的有效范围内,我们重写了 stillValid。
WARNING
请确保传递给 stillValid 作为参数的 Block 就是打开该菜单的方块! 如果没有这样做,菜单和屏幕可能会在打开后立即自行关闭。
最后,为了防止物品被吞掉,如 removed 方法中所示,在屏幕关闭时将输入的物品丢落回世界是非常重要的。
INFO
你可能已经注意到,有多个方法中包含了 ContainerLevelAccess#execute 调用。 这是 Mojang 使用的一个包装类,用于确保在发生交互时使用的是正确的 Level 和位置,并防止玩家访问他们不应该访问的容器。 注意,当对特殊的 NULL ContainerLevelAccess 调用 execute 时,它不会执行任何操作。
Slot 的 mayPlace 方法返回 false,这样玩家就无法将物品放入结果槽位;而 isFake 方法则告诉 Screen 其包含的物品堆(暂时)没有所有者。
你还需要将菜单添加到注册表中:
java
public static final MenuType<UpgradingMenu> UPGRADING_MENU_TYPE = register("upgrading", UpgradingMenu::new);1
最后,我们需要注册我们的方块:
java
public static final BlockItemId UPGRADING_BLOCK = create("upgrading_block");1
java
public static final Block UPGRADING_BLOCK = register(
ModBlockItemIds.UPGRADING_BLOCK, UpgradingBlock::new, BlockBehaviour.Properties.of()
);1
2
3
2
3
实现 quickMoveStack
INFO
另请参阅:容器菜单:创建菜单
在菜单中按住 Shift 键进行点击时,就会调用快速移动。
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
哇,又是好多代码。 让我们来试着梳理一下这里发生了什么。
通常,当从物品栏区域快速移动物品堆时,菜单首先会检查被点击的槽位是否是结果槽位(索引为 0)。 如果是,菜单会尝试将结果物品堆移动到物品栏中;如果移动失败,则什么也不会发生。
接下来,菜单会检查被点击的槽位是否属于物品栏。 如果是,菜单就会尝试将物品堆移动到输入槽位中。 如果移动失败,我们会尝试在物品栏内部移动该物品堆(在快捷栏中点击槽位会将其物品堆移动到物品栏的其他 27 个槽位中,反之亦然)。
如果被点击的槽位既不是结果槽位,也不在物品栏内,那么该槽位几乎可以确定是我们两个输入槽位中的一个,因此我们需要将其物品堆移回物品栏中。
屏幕
INFO
可另见容器菜单
目前,我们可以先借用原版铁砧的背景纹理。
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
别忘了在你的 ClientModInitializer 中将菜单类型绑定到屏幕,如下所示:
java
MenuScreens.register(ModMenuTypes.UPGRADING_MENU_TYPE, UpgradingScreen::new);1
配方余料
想要制作支持余料的配方吗? 我们建议你看看 net.minecraft.world.inventory.ResultSlot#getRemainingItems。 工作台使用此方法作为其结果槽位,因此可以找到许多与本文档相似之处,但也有一些差异。



