Робочі станки 26.2
Дізнайтеся, як створити робочий станок.
ПЕРЕДУМОВА
Цей робочий станок використовує власний тип рецепта, який можна знайти в розділі власних типів рецептів.
У цьому посібнику наведено інструкції зі створення власного робочого станка. На відміну від скринь, робочі станки не обов’язково повинні зберігати свій інвентар після закриття інтерфейсу (блоки на кшталт верстака не зберігають вміст інвентарю, тоді як інші, наприклад печі — зберігають). Для демонстрації ми не будемо використовувати блок-сутність.
Створення меню
INFO
Подробиці про створення меню див. у розділі меню контейнерів.
Щоб ми могли створити наш рецепт в інтерфейсі, ми створимо блок із меню. Щоб відкрити меню, нам потрібно буде перевизначити деякі методи в нашому класі 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
Щоб доповнити це меню, нам також знадобиться власний слот для результату.
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 ми створюємо екземпляр нашого вхідного класу рецепта, заповнюючи його двома вхідними слотами.
Щоб перевірити, чи відповідає він будь-яким рецептам, ми спочатку переконаємося, що ми знаходимося на рівні сервера, оскільки клієнти не знають, які існують рецепти. Потім ми отримаємо RecipeManager через serverLevel.recipeAccess().
Відступ: Синхронізація рецептів
Якщо клієнт не знає, які існують рецепти, то як працює книга рецептів?
Чудово, що ви запитали. Сервер повідомляє клієнту, які рецепти доступні, на основі тих, що ви вже розблокували (це відбувається після виконання певних умов, описаних у JSON-файлі прогресу для кожного рецепта — наприклад, отримання предмета або вхід у воду (для човнів)). Однак це створює чималі незручності для модів, що показують рецепти: в ідеалі вони мали б бачити всі доступні рецепти, але натомість можуть бачити лише ті, які клієнт отримує від сервера. Щоб обійти це, ми можемо використати Fabric API для синхронізації наших рецептів.
Ми викличемо serverLevel.recipeAccess().getRecipeFor з нашим уведенням рецепта, щоб отримати рецепт, який відповідає введеним даним. Якщо рецепт знайдено, ми можемо додати або видалити результат із результату контейнера.
Щоб виявити момент, коли користувач забирає результат, ми використовуємо перевизначення методу onTake у класі UpgradingResultSlot. Метод onTake нашого меню потім зменшує кількість вхідних предметів.
Щоб переконатися, що гравець перебуває в межах радіуса взаємодії з блоком, ми перевизначаємо метод stillValid.
WARNING
Переконайтеся, що Block, який ви передаєте як аргумент для stillValid — це саме той блок, що відкриває меню! Якщо цього не зробити, меню та екран можуть відкритися й одразу ж закритися.
Нарешті, щоб запобігти видаленню предметів, важливо скинути введені дані, коли екран закрито, як показано в методі removed.
INFO
Можливо, ви помітили, що кілька методів містять виклик ContainerLevelAccess#execute. Це клас-обгортка, який Mojang використовує для забезпечення використання правильного Level і позиції під час взаємодії, а також для запобігання доступу гравців до контейнерів, до яких вони не повинні мати доступу. Зауважте, що спеціальний об'єкт NULL ContainerLevelAccess не виконує жодних дій під час виклику методу execute.
Метод mayPlace класу Slot повертає 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. Верстак використовує це як слот для результату, тому можна знайти чимало спільного з документацією, але є й певні відмінності.



