工作站 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;
@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
为了配合这个菜单,我们还需要一个自定义的输出结果槽位 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。 工作台使用此方法作为其结果槽位,因此可以找到许多与本文档相似之处,但也有一些差异。



