Arbeitsplätze 26.2
Lerne, wie du Arbeitsplätze erstellst.
VORAUSSETZUNGEN
Dieser Arbeitsplatz verwendet einen benutzerdefinierten Rezepttyp, der unter Benutzerdefinierte Rezepttypen zu finden ist.
Dieses Tutorial bietet dir Anleitungen, wie du benutzerdefinierte Arbeitsplätze erstellst. Im Gegensatz zu Truhen müssen Arbeitsplätze ihr Inventar nicht unbedingt beibehalten, nachdem die Benutzeroberfläche geschlossen wurde (Blöcke wie die Werkbank speichern ihren Inhalt nicht, andere Blöcke wie beispielsweise Öfen hingegen schon). Zu Demonstrationszwecken werden wir keine Block Entität verwenden.
Ein Menü erstellen
INFO
Weitere Informationen zum Erstellen von Menüs findest du unter unter Container-Menüs.
Damit wir unser Rezept in dem GUI herstellen können, werden wir einen Block mit einem Menü erstellen. Um das Menü zu öffnen, müssen wir einige Methoden in unserer Klasse Block überschreiben:
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
Danach sind wir bereit, das Menü zu erstellen.
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
Passend zu diesem Menü benötigen wir außerdem einen benutzerdefinierten Slot für das Ergebnis.
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
Da gibt es eine Menge zu besprechen! Dieses Menü verfügt über zwei Eingabeslots und einen Ausgabeslot UpgradingResultSlot.
Der Eingabecontainer ist eine anonyme Unterklasse von SimpleContainer, die bei einer Änderung ihrer Items die Methode slotsChanged des Menüs aufruft. In slotsChanged erstellen wir dann eine Instanz unserer Rezept-Eingabeklasse und füllen sie mit den beiden Eingabeslots.
Um zu prüfen, ob es mit einem Rezept übereinstimmt, stellen wir zunächst sicher, dass wir uns auf der Serverebene befinden, da Clients nicht wissen, welche Rezepte vorhanden sind. Anschließend rufen wir den RecipeManager über serverLevel.recipeAccess() ab.
Eine Anmerkung am Rande: Rezeptsynchronisierung
Wenn der Client nicht weiß, welche Rezepte es gibt, wie funktioniert dann das Rezeptbuch?
Ich bin froh, dass du gefragt hast. Der Server teilt dem Client mit, welche Rezepte verfügbar sind, je nachdem, welche Rezepte du freigeschaltet hast (dies geschieht durch das Erfüllen bestimmter Kriterien, die in der JSON-Datei zum Fortschritt des jeweiligen Rezepts beschrieben sind, wie beispielsweise das Erlangen eines Items oder das Betreten von Wasser (bei Booten)). Das ist jedoch ziemlich ärgerlich für Mods zur Anzeige von Rezepten, die im Idealfall alle verfügbaren Rezepte anzeigen möchten, nun aber nur noch die Rezepte sehen können, die der Client vom Server erhält. Um dies zu umgehen, können wir die Fabric-API zur Synchronisierung unserer Rezepte verwenden.
Wir rufen serverLevel.recipeAccess().getRecipeFor mit unseren Rezept-Eingaben auf, um ein Rezept zu erhalten, das den Eingaben entspricht. Wenn ein Rezept gefunden wurde, können wir das Ergebnis zum Ergebniscontainer hinzufügen oder daraus entfernen.
Um zu erkennen, wann der Benutzer das Ergebnis entnimmt, verwenden wir die Überschreibung von onTakedes UpgradingResultSlot. Die Methode onTake unseres Menüs verringert dann die Anzahl der Eingabeitems.
Um sicherzustellen, dass sich der Spieler innerhalb der Interaktionsreichweite des Blocks befindet, überschreiben wir stillValid.
WARNING
Stelle sicher, dass der Block, den du als Argument für stillValid übergibst, der Block ist, der das Menü öffnet! Wenn du das nicht tust, werden das Menü und die Oberfläche möglicherweise geöffnet und schließen sich anschließend sofort wieder.
Um schließlich zu verhindern, dass Items gelöscht werden, ist es wichtig, die Eingabeslots beim Schließen der Oberfläche wieder zurückzusetzen, wie in der Methode removed gezeigt.
INFO
Möglicherweise ist dir aufgefallen, dass mehrere Methoden einen Aufruf von ContainerLevelAccess#execute enthalten. Dies ist eine Wrapper-Klasse, die von Mojang verwendet wird, um sicherzustellen, dass bei Interaktionen das richtige Level und die richtige Position verwendet werden und um zu verhindern, dass Spieler auf Container zugreifen, auf die sie keinen Zugriff haben sollten. Beachte, dass der spezielle NULL-ContainerLevelAccess keine Aktion ausführt, wenn die Methode execute auf ihm aufgerufen wird.
Die Methode mayPlace des Slot gibt false zurück, sodass Spieler keine Items in den Ergebnis-Slot einfügen können, und die Methode isFake teilt dem Screen mit, dass der darin enthaltene Stack (noch) keinen Besitzer hat.
Du musst auch das Menü zur Registry hinzufügen:
java
public static final MenuType<UpgradingMenu> UPGRADING_MENU_TYPE = register("upgrading", UpgradingMenu::new);1
Schließlich müssen wir unseren Block registrieren:
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
Implementieren von quickMoveStack
INFO
Siehe auch: Container Menüs: Das Menü erstellen
Quick Move wird immer dann aufgerufen, wenn in einem Menü ein Shift-Klick ausgeführt wird.
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, das ist wieder viel Code. Lasst uns einmal versuchen, die Situation zu durchdenken.
Wenn ein Stack aus dem Inventarbereich schnell verschoben wird, prüft das Menü in der Regel zunächst, ob es sich bei dem angeklickten Slot um den Ergebnisslot (mit Index 0) handelt. Wenn dies der Fall ist, versucht das Menü, den Ergebnisstack in das Inventar zu verschieben, aber sollte dies fehlschlagen, geschieht nichts.
Anschließend prüft das Menü, ob der angeklickte Slot zum Inventar gehört. Wenn dies so ist, versucht das Menü, den Stack in die Eingaben zu verschieben. Sollte dies fehlschlagen, versuchen wir, den Stack innerhalb des Inventars zu verschieben (durch Anklicken eines Slots in der Schnellzugriffsleiste werden die darin enthaltenen Stacks in die anderen 27 Slots des Inventars verschoben und umgekehrt).
Wenn der angeklickte Slot weder der Ergebnisslot noch ein Slot im Inventar war, handelt es sich bei diesem Slot mit ziemlicher Sicherheit um einen unserer beiden Eingabeslots, sodass wir den dortigen Stack wieder ins Inventar verschieben möchten.
Die Oberfläche
INFO
Siehe auch: Container Menüs: Die Oberfläche erstellen
Für den Moment können wir einfach die Hintergrundtextur des Vanilla Amboss übernehmen.
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
Vergiss nicht, deinen Menütyp in deinem ClientModInitializer wie folgt an die Oberfläche zu binden:
java
MenuScreens.register(ModMenuTypes.UPGRADING_MENU_TYPE, UpgradingScreen::new);1
Reste aus Rezepten
Möchtest du ein Rezept erstellen, das Reste berücksichtigt? Wir empfehlen, einen Blick auf net.minecraft.world.inventory.ResultSlot#getRemainingItems zu werfen. Die Werkbank verwendet dies als dessen Ergebnislot, sodass viele Ähnlichkeiten zur Dokumentation zu finden sind, es gibt jedoch auch einige Unterschiede.



