From a672a16f0b9751e21c6469e946a8eea2ea5d5080 Mon Sep 17 00:00:00 2001 From: frostbite-bot <304862314+frostbite-bot@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:44:03 +0000 Subject: [PATCH] Add network-aware terminal recipe book --- .../gui/DimensionsTerminalCraftGUI.java | 141 +++++++++- .../common/menu/BDBaseMenu.java | 58 +++- .../common/menu/DimensionsCraftMenu.java | 265 +++++++++++++++++- .../common/menu/DimensionsNetMenu.java | 18 +- 4 files changed, 473 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/wintercogs/beyonddimensions/client/gui/DimensionsTerminalCraftGUI.java b/src/main/java/com/wintercogs/beyonddimensions/client/gui/DimensionsTerminalCraftGUI.java index 8f7a3a2f..73c1471a 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/client/gui/DimensionsTerminalCraftGUI.java +++ b/src/main/java/com/wintercogs/beyonddimensions/client/gui/DimensionsTerminalCraftGUI.java @@ -1,11 +1,22 @@ package com.wintercogs.beyonddimensions.client.gui; import com.wintercogs.beyonddimensions.common.menu.DimensionsCraftMenuTerminal; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.client.gui.screens.recipebook.RecipeBookComponent; +import net.minecraft.client.gui.screens.recipebook.RecipeUpdateListener; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.inventory.Slot; +import org.jetbrains.annotations.NotNull; -public class DimensionsTerminalCraftGUI extends DimensionsCraftGUI +public class DimensionsTerminalCraftGUI extends DimensionsCraftGUI implements RecipeUpdateListener { + private final RecipeBookComponent recipeBookComponent = new RecipeBookComponent(); + private boolean widthTooNarrow; + private ImageButton recipeBookButton; public DimensionsTerminalCraftGUI(DimensionsCraftMenuTerminal container, Inventory playerInventory, Component title) { @@ -16,11 +27,137 @@ public DimensionsTerminalCraftGUI(DimensionsCraftMenuTerminal container, Invento protected void init() { super.init(); + widthTooNarrow = width < imageWidth + 203; + recipeBookComponent.init(width, height, minecraft, widthTooNarrow, menu); + int previousLeft = leftPos; + leftPos = recipeBookComponent.updateScreenPosition(width, imageWidth); + moveMainWidgets(leftPos - previousLeft); + recipeBookButton = new ImageButton( + leftPos + 5, + topPos + imageHeight - 112, + 20, + 18, + RecipeBookComponent.RECIPE_BUTTON_SPRITES, + button -> + { + recipeBookComponent.toggleVisibility(); + int oldLeft = leftPos; + leftPos = recipeBookComponent.updateScreenPosition(width, imageWidth); + moveMainWidgets(leftPos - oldLeft); + recipeBookButton.setPosition(leftPos + 5, topPos + imageHeight - 112); + } + ); + addRenderableWidget(recipeBookButton); + addWidget(recipeBookComponent); + } + + private void moveMainWidgets(int offset) + { + if (offset == 0) return; + for (var child : children()) + { + if (child instanceof AbstractWidget widget && widget != recipeBookButton) + widget.setX(widget.getX() + offset); + } + } + + @Override + public void containerTick() + { + super.containerTick(); + recipeBookComponent.tick(); + } + + @Override + public void render(@NotNull GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick) + { + if (recipeBookComponent.isVisible() && widthTooNarrow) + { + renderBackground(guiGraphics, mouseX, mouseY, partialTick); + recipeBookComponent.render(guiGraphics, mouseX, mouseY, partialTick); + } + else + { + super.render(guiGraphics, mouseX, mouseY, partialTick); + recipeBookComponent.render(guiGraphics, mouseX, mouseY, partialTick); + recipeBookComponent.renderGhostRecipe(guiGraphics, leftPos, topPos, true, partialTick); + } + + recipeBookComponent.renderTooltip(guiGraphics, leftPos, topPos, mouseX, mouseY); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) + { + return recipeBookComponent.keyPressed(keyCode, scanCode, modifiers) + || super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char codePoint, int modifiers) + { + return recipeBookComponent.charTyped(codePoint, modifiers) + || super.charTyped(codePoint, modifiers); + } + + @Override + protected boolean isHovering(int x, int y, int width, int height, double mouseX, double mouseY) + { + return (!widthTooNarrow || !recipeBookComponent.isVisible()) + && super.isHovering(x, y, width, height, mouseX, mouseY); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) + { + if (recipeBookComponent.mouseClicked(mouseX, mouseY, button)) + { + setFocused(recipeBookComponent); + return true; + } + return widthTooNarrow && recipeBookComponent.isVisible() + || super.mouseClicked(mouseX, mouseY, button); + } + + @Override + protected boolean hasClickedOutside(double mouseX, double mouseY, int guiLeft, int guiTop, int mouseButton) + { + boolean outside = mouseX < guiLeft + || mouseY < guiTop + || mouseX >= guiLeft + imageWidth + || mouseY >= guiTop + imageHeight; + return recipeBookComponent.hasClickedOutside( + mouseX, + mouseY, + leftPos, + topPos, + imageWidth, + imageHeight, + mouseButton + ) && outside; + } + + @Override + protected void slotClicked(Slot slot, int slotId, int mouseButton, ClickType type) + { + super.slotClicked(slot, slotId, mouseButton, type); + recipeBookComponent.slotClicked(slot); + } + + @Override + public void recipesUpdated() + { + recipeBookComponent.recipesUpdated(); + } + + @Override + public RecipeBookComponent getRecipeBookComponent() + { + return recipeBookComponent; } @Override protected void addCraftButton() { - // 清空 } } diff --git a/src/main/java/com/wintercogs/beyonddimensions/common/menu/BDBaseMenu.java b/src/main/java/com/wintercogs/beyonddimensions/common/menu/BDBaseMenu.java index 5f95c6f9..63a33b81 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/common/menu/BDBaseMenu.java +++ b/src/main/java/com/wintercogs/beyonddimensions/common/menu/BDBaseMenu.java @@ -12,8 +12,12 @@ import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.inventory.*; import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; import net.neoforged.neoforge.network.PacketDistributor; import org.anti_ad.mc.ipn.api.IPNIgnore; import org.jetbrains.annotations.NotNull; @@ -25,7 +29,7 @@ // 定义一些用于 超越维度 模组的ui界面的基本方法。 // 主要是重写网络同步和点击事件,确保父类机制不处理StoredStackSlot的相关内容 @IPNIgnore -public abstract class BDBaseMenu extends AbstractContainerMenu +public abstract class BDBaseMenu extends RecipeBookMenu { public final Player player; @@ -47,6 +51,58 @@ protected BDBaseMenu(@Nullable MenuType menuType, int containerId, Inventory this.player = playerInventory.player; } + @Override + public void fillCraftSlotsStackedContents(StackedContents itemHelper) + { + } + + @Override + public void clearCraftingContent() + { + } + + @Override + public boolean recipeMatches(RecipeHolder recipe) + { + return false; + } + + @Override + public int getResultSlotIndex() + { + return -1; + } + + @Override + public int getGridWidth() + { + return 0; + } + + @Override + public int getGridHeight() + { + return 0; + } + + @Override + public int getSize() + { + return 0; + } + + @Override + public RecipeBookType getRecipeBookType() + { + return RecipeBookType.CRAFTING; + } + + @Override + public boolean shouldMoveToInventory(int slotIndex) + { + return false; + } + protected void addSlotGroupSync(SlotGroupSync slotGroupSync) { slotGroupSyncs.add(slotGroupSync); diff --git a/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsCraftMenu.java b/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsCraftMenu.java index 4e192367..b3ce31a0 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsCraftMenu.java +++ b/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsCraftMenu.java @@ -7,6 +7,7 @@ import com.wintercogs.beyonddimensions.api.storage.handler.impl.UnorderedStackHandlerRemoveZero; import com.wintercogs.beyonddimensions.api.storage.key.IStackKey; import com.wintercogs.beyonddimensions.api.storage.key.KeyAmount; +import com.wintercogs.beyonddimensions.api.storage.key.impl.EmptyStackKey; import com.wintercogs.beyonddimensions.api.storage.key.impl.ItemStackKey; import com.wintercogs.beyonddimensions.common.menu.widget.slot.AbstractStackTypedSlot; import com.wintercogs.beyonddimensions.common.menu.widget.slot.AutoRefillResultSlot; @@ -22,14 +23,18 @@ import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.network.protocol.game.ClientboundPlaceGhostRecipePacket; +import net.minecraft.recipebook.PlaceRecipe; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.Container; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.player.StackedContents; import net.minecraft.world.inventory.*; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.CraftingInput; import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.Ingredient; import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.item.crafting.RecipeType; import net.minecraft.world.level.Level; @@ -38,7 +43,10 @@ import org.jetbrains.annotations.NotNull; import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.function.Supplier; @@ -79,8 +87,7 @@ public DimensionsCraftMenu(int id, Inventory playerInventory, FriendlyByteBuf da */ public DimensionsCraftMenu(MenuType type, int id, Inventory playerInventory, AbstractUnorderedStackHandler data, @Nullable NonNullList craftItems, @Nullable BlockPos entityPos) { - // 利用父类函数处理存储槽位 玩家背包 和一些其他数据添加处理 - super(type, id, playerInventory, data); + super(type, id, playerInventory, data, true); TransientCraftingContainer craftContainer; if (craftItems != null) @@ -110,6 +117,8 @@ public void setChanged() } }; initCraftSlots(playerInventory, craftContainer); + addPlayerInv(playerInventory); + addStorageSlots(); } @@ -236,6 +245,258 @@ public static Optional> getRecipe(Player player, Cr return level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, input, level); } + @Override + protected void onNetworkStorageChanged() + { + if (player.level().isClientSide()) + player.getInventory().setChanged(); + } + + @Override + public void fillCraftSlotsStackedContents(StackedContents itemHelper) + { + craftSlots.fillStackedContents(itemHelper); + for (KeyAmount entry : storage.getStorage()) + { + if (entry == null || entry.isEmpty() || !(entry.key() instanceof ItemStackKey itemKey)) continue; + + int count = (int) Math.min(entry.amount(), itemKey.getVanillaMaxStackSize()); + itemHelper.accountStack(itemKey.copyStackWithCount(count), count); + } + } + + @Override + public void clearCraftingContent() + { + craftSlots.clearContent(); + resultSlots.clearContent(); + } + + @Override + public boolean recipeMatches(RecipeHolder recipe) + { + return recipe.value().matches(craftSlots.asCraftInput(), player.level()); + } + + @Override + public int getResultSlotIndex() + { + return resultSlotIndex; + } + + @Override + public int getGridWidth() + { + return craftSlots.getWidth(); + } + + @Override + public int getGridHeight() + { + return craftSlots.getHeight(); + } + + @Override + public int getSize() + { + return craftSlotEndIndex; + } + + @Override + public RecipeBookType getRecipeBookType() + { + return RecipeBookType.CRAFTING; + } + + @Override + public boolean shouldMoveToInventory(int slotIndex) + { + return slotIndex >= craftSlotStartIndex && slotIndex < craftSlotEndIndex; + } + + @Override + public void handlePlacement(boolean placeAll, RecipeHolder recipe, ServerPlayer player) + { + Optional> selected = player.level().getRecipeManager() + .getAllRecipesFor(RecipeType.CRAFTING) + .stream() + .filter(holder -> holder.id().equals(recipe.id())) + .findFirst(); + + if (selected.isEmpty() + || !player.getRecipeBook().contains(selected.get()) + || !selected.get().value().canCraftInDimensions(getGridWidth(), getGridHeight())) + { + return; + } + + RecipeTransferPlan plan = createRecipeTransferPlan(selected.get(), placeAll); + if (plan == null) + { + player.connection.send(new ClientboundPlaceGhostRecipePacket(containerId, selected.get())); + return; + } + + transferRecipe(plan.keys(), plan.amounts()); + player.getInventory().setChanged(); + } + + private @Nullable RecipeTransferPlan createRecipeTransferPlan(RecipeHolder recipe, boolean placeAll) + { + List available = collectAvailableItems(); + int maxAmount = placeAll ? getMaximumTransferAmount(recipe, available) : 1; + if (maxAmount <= 0) return null; + + List selected = selectIngredients(recipe.value().getIngredients(), available, maxAmount); + if (selected == null) return null; + + ArrayList> keys = new ArrayList<>(); + ArrayList amounts = new ArrayList<>(); + for (int i = 0; i < craftSlots.getContainerSize(); i++) + { + keys.add(EmptyStackKey.INSTANCE); + amounts.add(0L); + } + + new PlaceRecipe() + { + @Override + public void addItemToSlot(SelectedIngredient ingredient, int slot, int amount, int x, int y) + { + int craftIndex = slot - craftSlotStartIndex; + if (craftIndex >= 0 && craftIndex < keys.size()) + { + keys.set(craftIndex, ingredient.key()); + amounts.set(craftIndex, (long) maxAmount); + } + } + }.placeRecipe(getGridWidth(), getGridHeight(), resultSlotIndex, recipe, selected.iterator(), maxAmount); + + return new RecipeTransferPlan(keys, amounts); + } + + private int getMaximumTransferAmount(RecipeHolder recipe, List available) + { + int upperBound = 64; + for (int amount = upperBound; amount > 0; amount--) + { + if (selectIngredients(recipe.value().getIngredients(), copyAvailableItems(available), amount) != null) + return amount; + } + return 0; + } + + private @Nullable List selectIngredients(List ingredients, List available, int amount) + { + ArrayList selected = new ArrayList<>(ingredients.size()); + for (Ingredient ingredient : ingredients) + { + if (ingredient.isEmpty()) + { + selected.add(SelectedIngredient.EMPTY); + continue; + } + + AvailableItem best = null; + for (AvailableItem item : available) + { + if (item.remaining() >= amount + && item.key().getVanillaMaxStackSize() >= amount + && ingredient.test(item.key().copyStack()) + && (best == null || item.remaining() > best.remaining())) + { + best = item; + } + } + if (best == null) return null; + + best.consume(amount); + selected.add(new SelectedIngredient(best.key())); + } + return selected; + } + + private List collectAvailableItems() + { + Map available = new HashMap<>(); + for (ItemStack stack : craftSlots.getItems()) addAvailableItem(available, stack); + for (ItemStack stack : player.getInventory().items) addAvailableItem(available, stack); + for (KeyAmount entry : storage.getStorage()) + { + if (entry != null && !entry.isEmpty() && entry.key() instanceof ItemStackKey itemKey) + addAvailableItem(available, itemKey, entry.amount()); + } + return new ArrayList<>(available.values()); + } + + private void addAvailableItem(Map available, ItemStack stack) + { + if (!stack.isEmpty()) addAvailableItem(available, new ItemStackKey(stack), stack.getCount()); + } + + private void addAvailableItem(Map available, ItemStackKey key, long amount) + { + AvailableItem matching = available.values().stream() + .filter(item -> item.key().isSameTypeSameComponents(key)) + .findFirst() + .orElse(null); + if (matching == null) + available.put(key, new AvailableItem(key, amount)); + else + matching.add(amount); + } + + private List copyAvailableItems(List available) + { + return available.stream().map(AvailableItem::copy).toList(); + } + + private record RecipeTransferPlan(List> keys, List amounts) + { + } + + private record SelectedIngredient(ItemStackKey key) + { + private static final SelectedIngredient EMPTY = new SelectedIngredient(null); + } + + private static final class AvailableItem + { + private final ItemStackKey key; + private long remaining; + + private AvailableItem(ItemStackKey key, long remaining) + { + this.key = key; + this.remaining = remaining; + } + + private ItemStackKey key() + { + return key; + } + + private long remaining() + { + return remaining; + } + + private void add(long amount) + { + remaining += amount; + } + + private void consume(long amount) + { + remaining -= amount; + } + + private AvailableItem copy() + { + return new AvailableItem(key, remaining); + } + } + public void transferRecipe(List> inputKeys, List amount) { // 清空工艺槽物品 diff --git a/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsNetMenu.java b/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsNetMenu.java index 7507da81..c86fbfbb 100644 --- a/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsNetMenu.java +++ b/src/main/java/com/wintercogs/beyonddimensions/common/menu/DimensionsNetMenu.java @@ -69,6 +69,11 @@ public DimensionsNetMenu(int id, Inventory playerInventory, FriendlyByteBuf data * @param data 存储信息 */ public DimensionsNetMenu(MenuType menuType, int id, Inventory playerInventory, AbstractUnorderedStackHandler data) + { + this(menuType, id, playerInventory, data, false); + } + + protected DimensionsNetMenu(MenuType menuType, int id, Inventory playerInventory, AbstractUnorderedStackHandler data, boolean deferSlots) { super(menuType, id, playerInventory); @@ -97,16 +102,21 @@ public DimensionsNetMenu(MenuType menuType, int id, Inventory playerInventory public void afterLoadChange() { updateViewerStorage(hasShiftDown); + onNetworkStorageChanged(); TooltipHelper.readAsCache(storage.getStorage(), Item.TooltipContext.of(player.level()), player, TooltipFlag.Default.NORMAL); TooltipHelper.readAsCache(storage.getStorage(), Item.TooltipContext.of(player.level()), player, TooltipFlag.Default.ADVANCED); } }); - // 添加玩家背包和快捷栏 - addPlayerInv(playerInventory); + if (!deferSlots) + { + addPlayerInv(playerInventory); + addStorageSlots(); + } + } - // 添加存储槽 - addStorageSlots(); + protected void onNetworkStorageChanged() + { } // 添加存储槽位