From 236a1fbe2db7845b3c5652cb2fcd74ce35a4135d Mon Sep 17 00:00:00 2001 From: GaLi <3096147684@qq.com> Date: Sun, 1 Mar 2026 15:58:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9F=BA=E7=A1=80=E7=A7=BB=E6=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/ModKeybindings.java | 39 +++ .../client/event/CtrlQPatternKeyHandler.java | 319 ++++++++++++++++++ .../com/extendedae_plus/init/ModNetwork.java | 2 + .../integration/jei/JeiClientBootstrap.java | 3 +- .../integration/jei/JeiRuntimeProxy.java | 48 +++ .../CreateAndUploadPatternC2SPacket.java | 216 ++++++++++++ .../network/CreateCtrlQPatternC2SPacket.java | 260 ++++++++++++++ .../RequestProvidersListC2SPacket.java | 22 ++ ...loadEncodedPatternToProviderC2SPacket.java | 9 + .../util/RecipeFinderUtil.java | 227 +++++++++++++ .../com/extendedae_plus/util/RecipeInfo.java | 98 ++++++ .../uploadPattern/CtrlQPendingUploadUtil.java | 220 ++++++++++++ .../ExtendedAEPatternUploadUtil.java | 61 ++++ .../assets/extendedae_plus/lang/en_us.json | 10 +- .../assets/extendedae_plus/lang/zh_cn.json | 10 +- 15 files changed, 1541 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/extendedae_plus/client/ModKeybindings.java create mode 100644 src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java create mode 100644 src/main/java/com/extendedae_plus/network/CreateAndUploadPatternC2SPacket.java create mode 100644 src/main/java/com/extendedae_plus/network/CreateCtrlQPatternC2SPacket.java create mode 100644 src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java create mode 100644 src/main/java/com/extendedae_plus/util/RecipeInfo.java create mode 100644 src/main/java/com/extendedae_plus/util/uploadPattern/CtrlQPendingUploadUtil.java diff --git a/src/main/java/com/extendedae_plus/client/ModKeybindings.java b/src/main/java/com/extendedae_plus/client/ModKeybindings.java new file mode 100644 index 0000000..365feff --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/ModKeybindings.java @@ -0,0 +1,39 @@ +package com.extendedae_plus.client; + +import com.extendedae_plus.ExtendedAEPlus; +import com.mojang.blaze3d.platform.InputConstants; +import net.minecraft.client.KeyMapping; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.fml.common.EventBusSubscriber; +import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent; +import net.neoforged.neoforge.client.settings.KeyConflictContext; +import net.neoforged.neoforge.client.settings.KeyModifier; +import org.lwjgl.glfw.GLFW; + +/** + * ExtendedAE Plus 快捷键定义 + */ +@EventBusSubscriber(modid = ExtendedAEPlus.MODID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) +public final class ModKeybindings { + private ModKeybindings() { + } + + /** + * Ctrl+Q 快速创建样板快捷键 + */ + public static final KeyMapping CREATE_PATTERN_KEY = new KeyMapping( + "key.extendedae_plus.create_pattern", + KeyConflictContext.GUI, + KeyModifier.CONTROL, + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_Q, + "key.categories.extendedae_plus" + ); + + @SubscribeEvent + public static void register(RegisterKeyMappingsEvent event) { + event.register(CREATE_PATTERN_KEY); + } +} + diff --git a/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java new file mode 100644 index 0000000..da44c85 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java @@ -0,0 +1,319 @@ +package com.extendedae_plus.client.event; + +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.extendedae_plus.client.ModKeybindings; +import com.extendedae_plus.integration.jei.JeiRuntimeProxy; +import com.extendedae_plus.network.CreateAndUploadPatternC2SPacket; +import com.extendedae_plus.network.CreateCtrlQPatternC2SPacket; +import com.extendedae_plus.network.RequestProvidersListC2SPacket; +import com.extendedae_plus.util.RecipeFinderUtil; +import com.extendedae_plus.util.RecipeInfo; +import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; +import mezz.jei.api.constants.RecipeTypes; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.ingredients.ITypedIngredient; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.client.event.ScreenEvent; +import net.neoforged.neoforge.network.PacketDistributor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Ctrl+Q 快速创建样板事件监听器 + */ +public final class CtrlQPatternKeyHandler { + private CtrlQPatternKeyHandler() { + } + + @SubscribeEvent + public static void onScreenKeyPressed(ScreenEvent.KeyPressed.Pre event) { + Screen screen = event.getScreen(); + if (screen == null) { + return; + } + + int keyCode = event.getKeyCode(); + int scanCode = event.getScanCode(); + if (!ModKeybindings.CREATE_PATTERN_KEY.matches(keyCode, scanCode)) { + return; + } + + if (JeiRuntimeProxy.get() == null) { + return; + } + + Optional recipeBookmark = JeiRuntimeProxy.getRecipeBookmarkUnderMouse(); + if (recipeBookmark.isPresent()) { + handleRecipeBookmark(recipeBookmark.get()); + event.setCanceled(true); + return; + } + + Optional> ingredient = castTypedIngredient(JeiRuntimeProxy.getIngredientUnderMouse()); + if (ingredient.isEmpty()) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.hover_item_first"), true); + } + return; + } + + List recipes = RecipeFinderUtil.findRecipesByIngredient(ingredient.get()); + if (recipes.isEmpty()) { + Minecraft mc = Minecraft.getInstance(); + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true); + } + return; + } + + RecipeInfo selected = RecipeFinderUtil.selectBestRecipe(recipes); + if (selected == null || selected.getRecipeId() == null) { + return; + } + + List selectedIngredients = selectIngredientsWithJeiPriority(selected); + List selectedOutputs = convertOutputsToItemStacks(selected); + + PacketDistributor.sendToServer(new CreateCtrlQPatternC2SPacket( + selected.getRecipeId(), + selected.isCraftingRecipe(), + selectedIngredients, + selectedOutputs + )); + event.setCanceled(true); + } + + private static void handleRecipeBookmark(Object recipeBookmark) { + if (isCraftingRecipe(recipeBookmark)) { + handleCraftingRecipeBookmark(recipeBookmark); + } else { + handleProcessingRecipeBookmark(recipeBookmark); + } + } + + private static boolean isCraftingRecipe(Object recipeBookmark) { + try { + var getRecipeCategoryMethod = recipeBookmark.getClass().getMethod("getRecipeCategory"); + Object recipeCategory = getRecipeCategoryMethod.invoke(recipeBookmark); + + var getRecipeTypeMethod = recipeCategory.getClass().getMethod("getRecipeType"); + Object recipeType = getRecipeTypeMethod.invoke(recipeCategory); + return RecipeTypes.CRAFTING.equals(recipeType) + || RecipeTypes.STONECUTTING.equals(recipeType) + || RecipeTypes.SMITHING.equals(recipeType); + } catch (Throwable ignored) { + return false; + } + } + + private static void handleCraftingRecipeBookmark(Object recipeBookmark) { + try { + ResourceLocation recipeId = getRecipeId(recipeBookmark); + if (recipeId == null) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) { + return; + } + var recipeOpt = mc.level.getRecipeManager().byKey(recipeId); + if (recipeOpt.isEmpty()) { + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), true); + } + return; + } + + List recipeInfos = findRecipeInfosForBookmark(recipeBookmark); + if (recipeInfos.isEmpty()) { + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true); + } + return; + } + + RecipeInfo matching = matchById(recipeInfos, recipeId); + List selectedIngredients = selectIngredientsWithJeiPriority(matching); + List selectedOutputs = convertOutputsToItemStacks(matching); + + PacketDistributor.sendToServer(new CreateAndUploadPatternC2SPacket( + recipeId, + matching.isCraftingRecipe(), + selectedIngredients, + selectedOutputs + )); + } catch (Throwable ignored) { + } + } + + private static void handleProcessingRecipeBookmark(Object recipeBookmark) { + try { + ResourceLocation recipeId = getRecipeId(recipeBookmark); + if (recipeId == null) { + return; + } + + Minecraft mc = Minecraft.getInstance(); + if (mc.level == null) { + return; + } + var recipeOpt = mc.level.getRecipeManager().byKey(recipeId); + if (recipeOpt.isEmpty()) { + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), true); + } + return; + } + + Object recipeBase = null; + try { + var getRecipeMethod = recipeBookmark.getClass().getMethod("getRecipe"); + recipeBase = getRecipeMethod.invoke(recipeBookmark); + } catch (Throwable ignored) { + } + setLastProcessingNameFromRecipe(recipeBase != null ? recipeBase : recipeOpt.get()); + + List recipeInfos = findRecipeInfosForBookmark(recipeBookmark); + if (recipeInfos.isEmpty()) { + if (mc.player != null) { + mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true); + } + return; + } + + RecipeInfo matching = matchById(recipeInfos, recipeId); + List selectedIngredients = selectIngredientsWithJeiPriority(matching); + List selectedOutputs = convertOutputsToItemStacks(matching); + + PacketDistributor.sendToServer(new CreateCtrlQPatternC2SPacket( + recipeId, + matching.isCraftingRecipe(), + selectedIngredients, + selectedOutputs, + true + )); + PacketDistributor.sendToServer(RequestProvidersListC2SPacket.INSTANCE); + } catch (Throwable ignored) { + } + } + + private static List findRecipeInfosForBookmark(Object recipeBookmark) { + Optional> hovered = castTypedIngredient(JeiRuntimeProxy.getIngredientUnderMouse()); + if (hovered.isPresent()) { + List infos = RecipeFinderUtil.findRecipesByIngredient(hovered.get()); + if (!infos.isEmpty()) { + return infos; + } + } + + try { + var getRecipeOutputMethod = recipeBookmark.getClass().getMethod("getRecipeOutput"); + Object recipeOutput = getRecipeOutputMethod.invoke(recipeBookmark); + if (recipeOutput instanceof ITypedIngredient typed) { + return RecipeFinderUtil.findRecipesByIngredient(typed); + } + } catch (Throwable ignored) { + } + return List.of(); + } + + private static RecipeInfo matchById(List recipeInfos, ResourceLocation recipeId) { + for (RecipeInfo info : recipeInfos) { + if (recipeId.equals(info.getRecipeId())) { + return info; + } + } + return recipeInfos.get(0); + } + + private static ResourceLocation getRecipeId(Object recipeBookmark) { + try { + var getRecipeUidMethod = recipeBookmark.getClass().getMethod("getRecipeUid"); + Object recipeId = getRecipeUidMethod.invoke(recipeBookmark); + if (recipeId instanceof ResourceLocation rl) { + return rl; + } + } catch (Throwable ignored) { + } + return null; + } + + @SuppressWarnings("unchecked") + private static Optional> castTypedIngredient(Optional opt) { + if (opt == null || opt.isEmpty()) { + return Optional.empty(); + } + Object value = opt.get(); + if (value instanceof ITypedIngredient) { + return (Optional>) (Optional) Optional.of(value); + } + return Optional.empty(); + } + + private static void setLastProcessingNameFromRecipe(Object recipeBase) { + String name = null; + if (recipeBase instanceof Recipe recipe) { + name = ExtendedAEPatternUploadUtil.mapRecipeTypeToSearchKey(recipe); + } else if (recipeBase != null + && "com.gregtechceu.gtceu.api.recipe.GTRecipe".equals(recipeBase.getClass().getName())) { + name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(recipeBase); + } else if (recipeBase != null + && "com.gregtechceu.gtceu.integration.jei.recipe.GTRecipeWrapper".equals(recipeBase.getClass().getName())) { + try { + var field = recipeBase.getClass().getField("recipe"); + Object inner = field.get(recipeBase); + name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(inner); + } catch (Throwable ignored) { + } + } + + if (name == null || name.isBlank()) { + name = ExtendedAEPatternUploadUtil.deriveSearchKeyFromUnknownRecipe(recipeBase); + } + if (name != null && !name.isBlank()) { + ExtendedAEPatternUploadUtil.setLastProcessingName(name); + } + } + + private static List selectIngredientsWithJeiPriority(RecipeInfo recipeInfo) { + List bookmarks = JeiRuntimeProxy.getBookmarkList(); + Map priorities = new HashMap<>(); + AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE); + + for (Object obj : bookmarks) { + if (obj instanceof ITypedIngredient ingredient) { + ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack -> + priorities.put(itemStack.getItem(), index.getAndDecrement()) + ); + } + } + + return recipeInfo.selectBestInputs(priorities); + } + + private static List convertOutputsToItemStacks(RecipeInfo recipeInfo) { + return recipeInfo.getOutputs().stream() + .map(genericStack -> { + if (genericStack.what() instanceof AEItemKey itemKey) { + return itemKey.toStack((int) genericStack.amount()); + } + return GenericStack.wrapInItemStack(genericStack); + }) + .toList(); + } +} + diff --git a/src/main/java/com/extendedae_plus/init/ModNetwork.java b/src/main/java/com/extendedae_plus/init/ModNetwork.java index d589e85..257bcdb 100644 --- a/src/main/java/com/extendedae_plus/init/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java @@ -22,6 +22,8 @@ public class ModNetwork { registrar.playToServer(OpenProviderUiC2SPacket.TYPE, OpenProviderUiC2SPacket.STREAM_CODEC, OpenProviderUiC2SPacket::handle); registrar.playToServer(UploadEncodedPatternToProviderC2SPacket.TYPE, UploadEncodedPatternToProviderC2SPacket.STREAM_CODEC, UploadEncodedPatternToProviderC2SPacket::handle); registrar.playToServer(UploadInventoryPatternToProviderC2SPacket.TYPE, UploadInventoryPatternToProviderC2SPacket.STREAM_CODEC, UploadInventoryPatternToProviderC2SPacket::handle); + registrar.playToServer(CreateCtrlQPatternC2SPacket.TYPE, CreateCtrlQPatternC2SPacket.STREAM_CODEC, CreateCtrlQPatternC2SPacket::handle); + registrar.playToServer(CreateAndUploadPatternC2SPacket.TYPE, CreateAndUploadPatternC2SPacket.STREAM_CODEC, CreateAndUploadPatternC2SPacket::handle); registrar.playToServer(EncodeWithShiftFlagC2SPacket.TYPE, EncodeWithShiftFlagC2SPacket.STREAM_CODEC, EncodeWithShiftFlagC2SPacket::handle); // 新增:JEI 中键打开合成界面 & 无线终端拾取方块物品 registrar.playToServer(com.extendedae_plus.network.OpenCraftFromJeiC2SPacket.TYPE, diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java b/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java index 3b8c7c4..214f44c 100644 --- a/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiClientBootstrap.java @@ -6,5 +6,6 @@ public final class JeiClientBootstrap { public static void register() { net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onMouseButtonPre); net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onKeyPressedPre); + net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.event.CtrlQPatternKeyHandler::onScreenKeyPressed); } -} \ No newline at end of file +} diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java index c4ecbec..fb88395 100644 --- a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java @@ -141,6 +141,54 @@ public final class JeiRuntimeProxy { return Collections.emptyList(); } + /** + * 获取鼠标下的配方书签(RecipeBookmark),用于 Ctrl+Q 直接识别“配方书签”而非普通配料。 + */ + public static Optional getRecipeBookmarkUnderMouse() { + Object rt = RUNTIME; + if (rt == null) return Optional.empty(); + try { + Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt); + if (overlay == null) return Optional.empty(); + + Object ingredientOpt = overlay.getClass().getMethod("getIngredientUnderMouse").invoke(overlay); + if (!(ingredientOpt instanceof Optional opt) || opt.isEmpty()) return Optional.empty(); + Object hoveredIngredient = opt.get(); + + Field f = overlay.getClass().getDeclaredField("bookmarkList"); + f.setAccessible(true); + Object bookmarkList = f.get(overlay); + if (bookmarkList == null) return Optional.empty(); + + Object elementsObj = bookmarkList.getClass().getMethod("getElements").invoke(bookmarkList); + if (!(elementsObj instanceof List elements)) return Optional.empty(); + + for (Object element : elements) { + if (element == null) continue; + Object typedIngredient = null; + try { + typedIngredient = element.getClass().getMethod("getTypedIngredient").invoke(element); + } catch (Throwable ignored) { + } + if (typedIngredient == null || !typedIngredient.equals(hoveredIngredient)) continue; + + Object bookmarkOpt = null; + try { + bookmarkOpt = element.getClass().getMethod("getBookmark").invoke(element); + } catch (Throwable ignored) { + } + if (bookmarkOpt instanceof Optional b && b.isPresent()) { + Object bookmark = b.get(); + if (bookmark != null && "RecipeBookmark".equals(bookmark.getClass().getSimpleName())) { + return Optional.of(bookmark); + } + } + } + } catch (Throwable ignored) { + } + return Optional.empty(); + } + public static void addBookmark(ItemStack stack) { Object rt = RUNTIME; if (rt == null || stack == null || stack.isEmpty()) return; diff --git a/src/main/java/com/extendedae_plus/network/CreateAndUploadPatternC2SPacket.java b/src/main/java/com/extendedae_plus/network/CreateAndUploadPatternC2SPacket.java new file mode 100644 index 0000000..1753851 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CreateAndUploadPatternC2SPacket.java @@ -0,0 +1,216 @@ +package com.extendedae_plus.network; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.energy.IEnergyService; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import appeng.api.storage.StorageHelper; +import appeng.core.definitions.AEItems; +import appeng.me.helpers.PlayerSource; +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil; +import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * C2S: 创建样板并上传到装配矩阵(合成书签分支)。 + */ +public class CreateAndUploadPatternC2SPacket implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(ExtendedAEPlus.MODID, "create_and_upload_pattern")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeResourceLocation(pkt.recipeId); + buf.writeBoolean(pkt.isCraftingPattern); + ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.selectedIngredients); + ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.outputs); + }, + buf -> { + ResourceLocation recipeId = buf.readResourceLocation(); + boolean isCraftingPattern = buf.readBoolean(); + List ingredients = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf); + List outputs = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf); + return new CreateAndUploadPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs); + } + ); + + private final ResourceLocation recipeId; + private final boolean isCraftingPattern; + private final List selectedIngredients; + private final List outputs; + + public CreateAndUploadPatternC2SPacket( + ResourceLocation recipeId, + boolean isCraftingPattern, + List selectedIngredients, + List outputs + ) { + this.recipeId = recipeId; + this.isCraftingPattern = isCraftingPattern; + this.selectedIngredients = selectedIngredients; + this.outputs = outputs; + } + + public static void handle(final CreateAndUploadPatternC2SPacket msg, final IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + + var recipeOpt = player.level().getRecipeManager().byKey(msg.recipeId); + if (recipeOpt.isEmpty()) { + player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), false); + return; + } + RecipeHolder recipeHolder = recipeOpt.get(); + + IGrid grid = CtrlQPendingUploadUtil.findPlayerGrid(player); + if (grid == null) { + player.displayClientMessage(Component.translatable("message.extendedae_plus.no_network"), false); + return; + } + + if (!consumeBlankPattern(player, grid)) { + player.displayClientMessage(Component.translatable("message.extendedae_plus.no_blank_pattern"), false); + return; + } + + ItemStack pattern = createPattern(recipeHolder, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player); + if (pattern.isEmpty()) { + refundBlankPattern(player, grid); + player.displayClientMessage(Component.translatable("message.extendedae_plus.pattern_creation_failed"), false); + return; + } + + boolean uploaded = ExtendedAEPatternUploadUtil.uploadPatternToMatrix(player, pattern, grid); + if (!uploaded) { + refundBlankPattern(player, grid); + } + }); + } + + @Override + public Type type() { + return TYPE; + } + + private static boolean consumeBlankPattern(ServerPlayer player, IGrid grid) { + AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack()); + IEnergyService energy = grid.getEnergyService(); + MEStorage storage = grid.getStorageService().getInventory(); + + long extracted = StorageHelper.poweredExtraction( + energy, + storage, + blankPatternKey, + 1, + new PlayerSource(player) + ); + return extracted > 0; + } + + private static void refundBlankPattern(ServerPlayer player, IGrid grid) { + AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack()); + IEnergyService energy = grid.getEnergyService(); + MEStorage storage = grid.getStorageService().getInventory(); + StorageHelper.poweredInsert( + energy, + storage, + blankPatternKey, + 1, + new PlayerSource(player) + ); + } + + private static ItemStack createPattern( + RecipeHolder recipeHolder, + boolean isCrafting, + List selectedIngredients, + List selectedOutputs, + ServerPlayer player + ) { + try { + if (isCrafting && recipeHolder.value() instanceof CraftingRecipe) { + @SuppressWarnings("unchecked") + RecipeHolder craftingHolder = (RecipeHolder) (RecipeHolder) recipeHolder; + + ItemStack[] inputs = new ItemStack[9]; + for (int i = 0; i < 9; i++) { + if (i < selectedIngredients.size()) { + inputs[i] = selectedIngredients.get(i).copy(); + } else { + inputs[i] = ItemStack.EMPTY; + } + } + + ItemStack output = ItemStack.EMPTY; + if (!selectedOutputs.isEmpty()) { + output = selectedOutputs.get(0).copy(); + } + if (output.isEmpty()) { + output = recipeHolder.value().getResultItem(player.level().registryAccess()).copy(); + } + + ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern( + craftingHolder, + inputs, + output, + true, + false + ); + CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName())); + return encodedPattern; + } + + List inputs = new ArrayList<>(); + List outputs = new ArrayList<>(); + + for (ItemStack item : selectedIngredients) { + if (!item.isEmpty()) { + GenericStack stack = GenericStack.unwrapItemStack(item); + if (stack == null) { + stack = GenericStack.fromItemStack(item); + } + if (stack != null) { + inputs.add(stack); + } + } + } + + for (ItemStack item : selectedOutputs) { + if (!item.isEmpty()) { + GenericStack stack = GenericStack.unwrapItemStack(item); + if (stack == null) { + stack = GenericStack.fromItemStack(item); + } + if (stack != null) { + outputs.add(stack); + } + } + } + + ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(inputs, outputs); + CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName())); + return encodedPattern; + } catch (Throwable ignored) { + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/com/extendedae_plus/network/CreateCtrlQPatternC2SPacket.java b/src/main/java/com/extendedae_plus/network/CreateCtrlQPatternC2SPacket.java new file mode 100644 index 0000000..c5bf75a --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CreateCtrlQPatternC2SPacket.java @@ -0,0 +1,260 @@ +package com.extendedae_plus.network; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.networking.IGrid; +import appeng.api.networking.energy.IEnergyService; +import appeng.api.stacks.GenericStack; +import appeng.api.storage.MEStorage; +import appeng.api.storage.StorageHelper; +import appeng.core.definitions.AEItems; +import appeng.items.tools.powered.WirelessTerminalItem; +import appeng.me.helpers.PlayerSource; +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil; +import com.extendedae_plus.util.wireless.WirelessTerminalLocator; +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * C2S: Ctrl+Q 快速创建样板请求。 + */ +public class CreateCtrlQPatternC2SPacket implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(ExtendedAEPlus.MODID, "create_ctrlq_pattern")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeResourceLocation(pkt.recipeId); + buf.writeBoolean(pkt.isCraftingPattern); + ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.selectedIngredients); + ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.outputs); + buf.writeBoolean(pkt.openProviderSelector); + }, + buf -> { + ResourceLocation recipeId = buf.readResourceLocation(); + boolean isCraftingPattern = buf.readBoolean(); + List ingredients = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf); + List outputs = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf); + + boolean openProviderSelector = buf.readableBytes() > 0 && buf.readBoolean(); + return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs, openProviderSelector); + } + ); + + private final ResourceLocation recipeId; + private final boolean isCraftingPattern; + private final List selectedIngredients; + private final List outputs; + private final boolean openProviderSelector; + + public CreateCtrlQPatternC2SPacket( + ResourceLocation recipeId, + boolean isCraftingPattern, + List selectedIngredients, + List outputs + ) { + this(recipeId, isCraftingPattern, selectedIngredients, outputs, false); + } + + public CreateCtrlQPatternC2SPacket( + ResourceLocation recipeId, + boolean isCraftingPattern, + List selectedIngredients, + List outputs, + boolean openProviderSelector + ) { + this.recipeId = recipeId; + this.isCraftingPattern = isCraftingPattern; + this.selectedIngredients = selectedIngredients; + this.outputs = outputs; + this.openProviderSelector = openProviderSelector; + } + + public static void handle(final CreateCtrlQPatternC2SPacket msg, final IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) { + return; + } + + var recipeOpt = player.level().getRecipeManager().byKey(msg.recipeId); + if (recipeOpt.isEmpty()) { + player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), false); + return; + } + RecipeHolder recipeHolder = recipeOpt.get(); + + if (!consumeBlankPattern(player)) { + player.displayClientMessage(Component.translatable("message.extendedae_plus.no_blank_pattern"), false); + return; + } + + ItemStack pattern = createPattern(recipeHolder, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player); + if (pattern.isEmpty()) { + player.getInventory().add(AEItems.BLANK_PATTERN.stack()); + player.displayClientMessage(Component.translatable("message.extendedae_plus.pattern_creation_failed"), false); + return; + } + + if (msg.openProviderSelector) { + String pendingId = CtrlQPendingUploadUtil.beginPendingCtrlQUpload(player, pattern); + if (pendingId == null) { + if (!player.getInventory().add(pattern)) { + player.drop(pattern, false); + } + } + return; + } + + if (!player.getInventory().add(pattern)) { + player.drop(pattern, false); + } + }); + } + + @Override + public Type type() { + return TYPE; + } + + private static boolean consumeBlankPattern(ServerPlayer player) { + if (tryExtractFromNetwork(player)) { + return true; + } + + Inventory inventory = player.getInventory(); + for (int i = 0; i < inventory.getContainerSize(); i++) { + ItemStack stack = inventory.getItem(i); + if (AEItems.BLANK_PATTERN.is(stack)) { + stack.shrink(1); + return true; + } + } + return false; + } + + private static boolean tryExtractFromNetwork(ServerPlayer player) { + WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player); + ItemStack terminal = located.stack; + if (terminal.isEmpty()) { + return false; + } + + WirelessTerminalItem wt = terminal.getItem() instanceof WirelessTerminalItem t ? t : null; + if (wt == null) { + return false; + } + + IGrid grid = wt.getLinkedGrid(terminal, player.serverLevel(), null); + if (grid == null) { + return false; + } + if (!wt.hasPower(player, 0.5, terminal)) { + return false; + } + + IEnergyService energy = grid.getEnergyService(); + MEStorage storage = grid.getStorageService().getInventory(); + long extracted = StorageHelper.poweredExtraction( + energy, + storage, + appeng.api.stacks.AEItemKey.of(AEItems.BLANK_PATTERN.stack()), + 1, + new PlayerSource(player) + ); + if (extracted > 0) { + wt.usePower(player, 0.5, terminal); + located.commit(); + return true; + } + return false; + } + + private static ItemStack createPattern( + RecipeHolder recipeHolder, + boolean isCrafting, + List selectedIngredients, + List selectedOutputs, + ServerPlayer player + ) { + try { + if (isCrafting && recipeHolder.value() instanceof CraftingRecipe) { + @SuppressWarnings("unchecked") + RecipeHolder craftingHolder = (RecipeHolder) (RecipeHolder) recipeHolder; + + ItemStack[] inputs = new ItemStack[9]; + for (int i = 0; i < 9; i++) { + if (i < selectedIngredients.size()) { + inputs[i] = selectedIngredients.get(i).copy(); + } else { + inputs[i] = ItemStack.EMPTY; + } + } + + ItemStack output = ItemStack.EMPTY; + if (!selectedOutputs.isEmpty()) { + output = selectedOutputs.get(0).copy(); + } + if (output.isEmpty()) { + output = recipeHolder.value().getResultItem(player.level().registryAccess()).copy(); + } + + ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern( + craftingHolder, + inputs, + output, + true, + false + ); + CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName())); + return encodedPattern; + } + + List inputs = new ArrayList<>(); + List outputs = new ArrayList<>(); + + for (ItemStack item : selectedIngredients) { + if (!item.isEmpty()) { + GenericStack stack = GenericStack.unwrapItemStack(item); + if (stack == null) { + stack = GenericStack.fromItemStack(item); + } + if (stack != null) { + inputs.add(stack); + } + } + } + + for (ItemStack item : selectedOutputs) { + if (!item.isEmpty()) { + GenericStack stack = GenericStack.unwrapItemStack(item); + if (stack == null) { + stack = GenericStack.fromItemStack(item); + } + if (stack != null) { + outputs.add(stack); + } + } + } + + ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(inputs, outputs); + CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName())); + return encodedPattern; + } catch (Throwable ignored) { + return ItemStack.EMPTY; + } + } +} diff --git a/src/main/java/com/extendedae_plus/network/RequestProvidersListC2SPacket.java b/src/main/java/com/extendedae_plus/network/RequestProvidersListC2SPacket.java index 2d82bc7..de38086 100644 --- a/src/main/java/com/extendedae_plus/network/RequestProvidersListC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/RequestProvidersListC2SPacket.java @@ -4,6 +4,7 @@ import appeng.helpers.patternprovider.PatternContainer; import appeng.menu.implementations.PatternAccessTermMenu; import appeng.menu.me.items.PatternEncodingTermMenu; import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil; import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -32,6 +33,27 @@ public class RequestProvidersListC2SPacket implements CustomPacketPayload { public static void handle(final RequestProvidersListC2SPacket msg, final IPayloadContext ctx) { ctx.enqueueWork(() -> { if (!(ctx.player() instanceof ServerPlayer player)) return; + + // Ctrl+Q pending 模式:不依赖编码终端,直接基于玩家网络给出列表(负数索引 ID) + if (CtrlQPendingUploadUtil.hasPendingCtrlQPattern(player)) { + List containers = CtrlQPendingUploadUtil.listAvailableProvidersFromPlayerNetwork(player); + List idxIds = new ArrayList<>(); + List names = new ArrayList<>(); + List slots = new ArrayList<>(); + for (int i = 0; i < containers.size(); i++) { + var c = containers.get(i); + if (c == null) continue; + int empty = ExtendedAEPatternUploadUtil.getAvailableSlots(c); + if (empty <= 0) continue; + long encodedId = -1L - i; + idxIds.add(encodedId); + names.add(ExtendedAEPatternUploadUtil.getProviderDisplayName(c)); + slots.add(empty); + } + player.connection.send(new ProvidersListS2CPacket(idxIds, names, slots)); + return; + } + if (!(player.containerMenu instanceof PatternEncodingTermMenu encMenu)) return; // 优先:若玩家也打开了样板访问终端,则用 byId 方式(精确服务器ID) diff --git a/src/main/java/com/extendedae_plus/network/UploadEncodedPatternToProviderC2SPacket.java b/src/main/java/com/extendedae_plus/network/UploadEncodedPatternToProviderC2SPacket.java index 44b0133..f665b9d 100644 --- a/src/main/java/com/extendedae_plus/network/UploadEncodedPatternToProviderC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/UploadEncodedPatternToProviderC2SPacket.java @@ -1,6 +1,7 @@ package com.extendedae_plus.network; import appeng.menu.me.items.PatternEncodingTermMenu; +import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil; import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.StreamCodec; @@ -29,6 +30,14 @@ public class UploadEncodedPatternToProviderC2SPacket implements CustomPacketPayl public static void handle(final UploadEncodedPatternToProviderC2SPacket msg, final IPayloadContext ctx) { ctx.enqueueWork(() -> { if (!(ctx.player() instanceof ServerPlayer player)) return; + + // 优先处理 Ctrl+Q pending 上传 + if (CtrlQPendingUploadUtil.hasPendingCtrlQPattern(player)) { + if (CtrlQPendingUploadUtil.uploadPendingCtrlQPattern(player, msg.providerId)) { + return; + } + } + if (!(player.containerMenu instanceof PatternEncodingTermMenu menu)) return; // 支持两种模式: // 1) providerId >= 0: 访问终端 byId 模式 diff --git a/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java new file mode 100644 index 0000000..d1890d0 --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java @@ -0,0 +1,227 @@ +package com.extendedae_plus.util; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import com.extendedae_plus.integration.jei.JeiRuntimeProxy; +import mezz.jei.api.constants.RecipeTypes; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.gui.IRecipeLayoutDrawable; +import mezz.jei.api.gui.ingredient.IRecipeSlotView; +import mezz.jei.api.gui.ingredient.IRecipeSlotsView; +import mezz.jei.api.helpers.IJeiHelpers; +import mezz.jei.api.ingredients.ITypedIngredient; +import mezz.jei.api.neoforge.NeoForgeTypes; +import mezz.jei.api.recipe.IFocus; +import mezz.jei.api.recipe.IFocusFactory; +import mezz.jei.api.recipe.IRecipeLookup; +import mezz.jei.api.recipe.IRecipeManager; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.RecipeType; +import mezz.jei.api.recipe.category.IRecipeCategory; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.neoforged.neoforge.fluids.FluidStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * 基于 JEI 运行时查找配方,提取输入输出槽位与数量信息。 + */ +public final class RecipeFinderUtil { + private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder"); + + private RecipeFinderUtil() { + } + + public static List findRecipesByIngredient(ITypedIngredient ingredient) { + if (ingredient == null) { + return List.of(); + } + + Object runtimeObj = JeiRuntimeProxy.get(); + if (!(runtimeObj instanceof IJeiRuntime jeiRuntime)) { + LOGGER.warn("[RecipeFinder] JEI runtime not available"); + return List.of(); + } + + IJeiHelpers jeiHelpers = jeiRuntime.getJeiHelpers(); + IRecipeManager recipeManager = jeiRuntime.getRecipeManager(); + IFocusFactory focusFactory = jeiHelpers.getFocusFactory(); + IFocus outputFocus = focusFactory.createFocus(RecipeIngredientRole.OUTPUT, ingredient); + List results = new ArrayList<>(); + + // 1) Crafting recipes + try { + IRecipeCategory> category = recipeManager.getRecipeCategory(RecipeTypes.CRAFTING); + recipeManager.createRecipeLookup(RecipeTypes.CRAFTING) + .limitFocus(List.of(outputFocus)) + .get() + .forEach(recipeHolder -> { + Optional> layoutOpt = createLayout(recipeManager, category, recipeHolder, focusFactory); + layoutOpt.ifPresent(layout -> { + RecipeInfo info = extractRecipeInfo(recipeHolder, layout, true); + if (info != null) { + results.add(info); + } + }); + }); + } catch (Throwable t) { + LOGGER.warn("[RecipeFinder] Error searching crafting recipes: {}", t.toString()); + } + + // 2) Other recipe types + try { + jeiHelpers.getAllRecipeTypes().forEach(recipeType -> { + if (recipeType.equals(RecipeTypes.CRAFTING)) { + return; + } + try { + IRecipeCategory category = recipeManager.getRecipeCategory(recipeType); + @SuppressWarnings({ "rawtypes", "unchecked" }) + IRecipeLookup lookup = (IRecipeLookup) recipeManager.createRecipeLookup((RecipeType) recipeType); + + lookup.limitFocus(List.of(outputFocus)) + .get() + .forEach(recipeObj -> { + Optional> layoutOpt = createLayout(recipeManager, category, recipeObj, focusFactory); + layoutOpt.ifPresent(layout -> { + RecipeInfo info = extractRecipeInfo(recipeObj, layout, false); + if (info != null) { + results.add(info); + } + }); + }); + } catch (Throwable ignored) { + } + }); + } catch (Throwable t) { + LOGGER.warn("[RecipeFinder] Error searching other recipes: {}", t.toString()); + } + + return results; + } + + public static RecipeInfo selectBestRecipe(List recipes) { + if (recipes == null || recipes.isEmpty()) { + return null; + } + for (RecipeInfo info : recipes) { + if (info.isCraftingRecipe()) { + return info; + } + } + return recipes.get(0); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Optional> createLayout( + IRecipeManager recipeManager, + IRecipeCategory category, + Object recipeObj, + IFocusFactory focusFactory + ) { + return (Optional) recipeManager.createRecipeLayoutDrawable( + (IRecipeCategory) category, + recipeObj, + focusFactory.getEmptyFocusGroup() + ); + } + + private static RecipeInfo extractRecipeInfo(Object recipeObj, IRecipeLayoutDrawable layout, boolean isCrafting) { + try { + ResourceLocation recipeId = extractRecipeId(recipeObj); + if (recipeId == null) { + return null; + } + + IRecipeSlotsView slotsView = layout.getRecipeSlotsView(); + List> inputs = new ArrayList<>(); + List outputs = new ArrayList<>(); + + for (IRecipeSlotView slot : slotsView.getSlotViews(RecipeIngredientRole.INPUT)) { + List slotStacks = new ArrayList<>(); + for (ITypedIngredient typed : slot.getAllIngredientsList()) { + if (typed == null) { + continue; + } + GenericStack stack = convertToGenericStack(typed); + if (stack != null) { + slotStacks.add(stack); + } + } + inputs.add(slotStacks); + } + + for (IRecipeSlotView slot : slotsView.getSlotViews(RecipeIngredientRole.OUTPUT)) { + for (ITypedIngredient typed : slot.getAllIngredientsList()) { + if (typed == null) { + continue; + } + GenericStack stack = convertToGenericStack(typed); + if (stack != null) { + outputs.add(stack); + } + } + } + + return new RecipeInfo(recipeObj, recipeId, isCrafting, inputs, outputs); + } catch (Throwable t) { + return null; + } + } + + private static ResourceLocation extractRecipeId(Object recipeObj) { + if (recipeObj == null) { + return null; + } + if (recipeObj instanceof RecipeHolder holder) { + return holder.id(); + } + try { + var m = recipeObj.getClass().getMethod("getId"); + Object id = m.invoke(recipeObj); + if (id instanceof ResourceLocation rl) { + return rl; + } + } catch (Throwable ignored) { + } + try { + var m = recipeObj.getClass().getMethod("getRecipeUid"); + Object id = m.invoke(recipeObj); + if (id instanceof ResourceLocation rl) { + return rl; + } + } catch (Throwable ignored) { + } + return null; + } + + private static GenericStack convertToGenericStack(ITypedIngredient typedIngredient) { + if (typedIngredient.getType() == VanillaTypes.ITEM_STACK) { + ItemStack itemStack = (ItemStack) typedIngredient.getIngredient(); + if (!itemStack.isEmpty()) { + AEItemKey itemKey = AEItemKey.of(itemStack); + if (itemKey != null) { + return new GenericStack(itemKey, itemStack.getCount()); + } + } + } else if (typedIngredient.getType() == NeoForgeTypes.FLUID_STACK) { + FluidStack fluidStack = (FluidStack) typedIngredient.getIngredient(); + if (!fluidStack.isEmpty()) { + AEFluidKey fluidKey = AEFluidKey.of(fluidStack); + if (fluidKey != null) { + return new GenericStack(fluidKey, fluidStack.getAmount()); + } + } + } + return null; + } +} diff --git a/src/main/java/com/extendedae_plus/util/RecipeInfo.java b/src/main/java/com/extendedae_plus/util/RecipeInfo.java new file mode 100644 index 0000000..2fdaf07 --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/RecipeInfo.java @@ -0,0 +1,98 @@ +package com.extendedae_plus.util; + +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 配方完整信息(包含配方ID、输入槽位候选项、输出) + */ +public class RecipeInfo { + private final Object recipeBase; + private final ResourceLocation recipeId; + private final boolean craftingRecipe; + private final List> inputs; + private final List outputs; + + public RecipeInfo( + Object recipeBase, + ResourceLocation recipeId, + boolean craftingRecipe, + List> inputs, + List outputs + ) { + this.recipeBase = recipeBase; + this.recipeId = recipeId; + this.craftingRecipe = craftingRecipe; + this.inputs = inputs; + this.outputs = outputs; + } + + public Object getRecipeBase() { + return recipeBase; + } + + public ResourceLocation getRecipeId() { + return recipeId; + } + + public boolean isCraftingRecipe() { + return craftingRecipe; + } + + public List> getInputs() { + return inputs; + } + + public List getOutputs() { + return outputs; + } + + public List selectBestInputs(Map bookmarkPriorities) { + List selected = new ArrayList<>(); + for (List slotOptions : inputs) { + if (slotOptions.isEmpty()) { + selected.add(ItemStack.EMPTY); + continue; + } + + GenericStack best = slotOptions.get(0); + int bestPriority = getPriority(best, bookmarkPriorities); + for (int i = 1; i < slotOptions.size(); i++) { + GenericStack option = slotOptions.get(i); + int priority = getPriority(option, bookmarkPriorities); + if (priority < bestPriority) { + bestPriority = priority; + best = option; + } + } + selected.add(toItemStack(best)); + } + return selected; + } + + private int getPriority(GenericStack stack, Map priorities) { + if (stack.what() instanceof AEItemKey itemKey) { + return priorities.getOrDefault(itemKey.getItem(), Integer.MAX_VALUE); + } + return Integer.MAX_VALUE; + } + + private ItemStack toItemStack(GenericStack stack) { + if (stack.what() instanceof AEItemKey itemKey) { + return itemKey.toStack((int) stack.amount()); + } + if (stack.what() instanceof AEFluidKey) { + return GenericStack.wrapInItemStack(stack); + } + return ItemStack.EMPTY; + } +} + diff --git a/src/main/java/com/extendedae_plus/util/uploadPattern/CtrlQPendingUploadUtil.java b/src/main/java/com/extendedae_plus/util/uploadPattern/CtrlQPendingUploadUtil.java new file mode 100644 index 0000000..2bc3fae --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/uploadPattern/CtrlQPendingUploadUtil.java @@ -0,0 +1,220 @@ +package com.extendedae_plus.util.uploadPattern; + +import appeng.api.crafting.PatternDetailsHelper; +import appeng.api.inventories.InternalInventory; +import appeng.api.networking.IGrid; +import appeng.helpers.patternprovider.PatternContainer; +import appeng.items.tools.powered.WirelessTerminalItem; +import appeng.util.inv.FilteredInternalInventory; +import appeng.util.inv.filter.IAEItemFilter; +import com.extendedae_plus.menu.locator.CuriosItemLocator; +import com.extendedae_plus.util.wireless.WirelessTerminalLocator; +import de.mari_023.ae2wtlib.api.registration.WTDefinition; +import de.mari_023.ae2wtlib.api.terminal.WTMenuHost; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.ItemStack; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +/** + * Ctrl+Q 临时样板缓存与上传逻辑(pending provider upload)。 + */ +public final class CtrlQPendingUploadUtil { + private static final String PENDING_DATA_KEY = "eap_ctrlq_pending_provider_upload_id"; + private static final String PENDING_STACK_KEY = "eap_ctrlq_pending_provider_upload_stack"; + + private CtrlQPendingUploadUtil() { + } + + public static String beginPendingCtrlQUpload(ServerPlayer player, ItemStack pattern) { + if (player == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) { + return null; + } + clearPendingCtrlQUpload(player); + String id = UUID.randomUUID().toString(); + player.getPersistentData().putString(PENDING_DATA_KEY, id); + player.getPersistentData().put(PENDING_STACK_KEY, pattern.saveOptional(player.registryAccess())); + return id; + } + + public static void clearPendingCtrlQUpload(ServerPlayer player) { + if (player == null) return; + player.getPersistentData().remove(PENDING_DATA_KEY); + player.getPersistentData().remove(PENDING_STACK_KEY); + } + + public static boolean hasPendingCtrlQPattern(ServerPlayer player) { + if (player == null) return false; + String id = player.getPersistentData().getString(PENDING_DATA_KEY); + if (id == null || id.isBlank()) return false; + return !getPendingCtrlQPattern(player).isEmpty(); + } + + public static boolean uploadPendingCtrlQPattern(ServerPlayer player, long providerId) { + if (player == null) return false; + ItemStack pending = getPendingCtrlQPattern(player); + if (pending.isEmpty()) return false; + + ItemStack remain = insertPatternIntoProviderFromPlayerNetwork(player, pending, providerId); + if (remain.getCount() >= pending.getCount()) { + return false; + } + + if (remain.isEmpty()) { + clearPendingCtrlQUpload(player); + } else { + player.getPersistentData().put(PENDING_STACK_KEY, remain.saveOptional(player.registryAccess())); + } + return true; + } + + public static List listAvailableProvidersFromPlayerNetwork(ServerPlayer player) { + return listAvailableProvidersFromGrid(findPlayerGrid(player)); + } + + public static IGrid findPlayerGrid(ServerPlayer player) { + WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player); + ItemStack terminal = located.stack; + if (terminal.isEmpty()) { + return null; + } + + WirelessTerminalItem wt = terminal.getItem() instanceof WirelessTerminalItem t ? t : null; + if (wt != null) { + return wt.getLinkedGrid(terminal, player.serverLevel(), null); + } + + String curiosSlotId = located.getCuriosSlotId(); + int curiosIndex = located.getCuriosIndex(); + if (curiosSlotId != null && curiosIndex >= 0) { + try { + WTDefinition def = WTDefinition.ofOrNull(terminal); + if (def == null) return null; + WTMenuHost wtHost = def.wTMenuHostFactory().create( + def.item(), + player, + new CuriosItemLocator(curiosSlotId, curiosIndex), + (p, sub) -> { + } + ); + if (wtHost == null || wtHost.getActionableNode() == null) return null; + return wtHost.getActionableNode().getGrid(); + } catch (Throwable ignored) { + return null; + } + } + + return null; + } + + public static List listAvailableProvidersFromGrid(IGrid grid) { + List list = new ArrayList<>(); + if (grid == null) return list; + try { + for (var machineClass : grid.getMachineClasses()) { + if (PatternContainer.class.isAssignableFrom(machineClass)) { + @SuppressWarnings("unchecked") + Class containerClass = (Class) machineClass; + for (var container : grid.getActiveMachines(containerClass)) { + if (container == null || !container.isVisibleInTerminal()) continue; + InternalInventory inv = container.getTerminalPatternInventory(); + if (inv == null || inv.size() <= 0) continue; + boolean hasEmpty = false; + for (int i = 0; i < inv.size(); i++) { + if (inv.getStackInSlot(i).isEmpty()) { + hasEmpty = true; + break; + } + } + if (hasEmpty) list.add(container); + } + } + } + } catch (Throwable ignored) { + } + return list; + } + + private static ItemStack getPendingCtrlQPattern(ServerPlayer player) { + if (player == null) return ItemStack.EMPTY; + String id = player.getPersistentData().getString(PENDING_DATA_KEY); + if (id == null || id.isBlank()) return ItemStack.EMPTY; + + CompoundTag data = player.getPersistentData(); + if (!data.contains(PENDING_STACK_KEY)) return ItemStack.EMPTY; + CompoundTag stackTag = data.getCompound(PENDING_STACK_KEY); + ItemStack stack = ItemStack.parseOptional(player.registryAccess(), stackTag); + if (stack.isEmpty() || !PatternDetailsHelper.isEncodedPattern(stack)) { + clearPendingCtrlQUpload(player); + return ItemStack.EMPTY; + } + return stack; + } + + private static ItemStack insertPatternIntoProviderFromPlayerNetwork(ServerPlayer player, ItemStack pattern, long providerId) { + if (player == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) { + return pattern == null ? ItemStack.EMPTY : pattern; + } + + int index = decodeProviderIndex(providerId); + if (index < 0) return pattern; + + List providers = listAvailableProvidersFromPlayerNetwork(player); + if (index >= providers.size()) return pattern; + + PatternContainer target = providers.get(index); + if (target == null) return pattern; + + ItemStack remain = pattern.copy(); + for (PatternContainer container : buildSameNameTryList(providers, target)) { + InternalInventory inv = container.getTerminalPatternInventory(); + if (inv == null || inv.size() <= 0) continue; + + ItemStack nextRemain = new FilteredInternalInventory(inv, new CtrlQPatternFilter()).addItems(remain.copy()); + if (nextRemain.getCount() < remain.getCount()) { + remain = nextRemain; + if (remain.isEmpty()) { + return ItemStack.EMPTY; + } + } + } + return remain; + } + + private static int decodeProviderIndex(long providerId) { + if (providerId >= 0) return -1; + long idx = -1L - providerId; + if (idx > Integer.MAX_VALUE) return -1; + return (int) idx; + } + + private static List buildSameNameTryList(List all, PatternContainer target) { + String targetName = ExtendedAEPatternUploadUtil.getProviderDisplayName(target); + List tryList = new ArrayList<>(); + tryList.add(target); + for (PatternContainer container : all) { + if (container == null || container == target) continue; + String name = ExtendedAEPatternUploadUtil.getProviderDisplayName(container); + if (name != null && name.equals(targetName)) { + tryList.add(container); + } + } + return tryList; + } + + private static class CtrlQPatternFilter implements IAEItemFilter { + @Override + public boolean allowExtract(InternalInventory inv, int slot, int amount) { + return true; + } + + @Override + public boolean allowInsert(InternalInventory inv, int slot, ItemStack stack) { + return !stack.isEmpty() && PatternDetailsHelper.isEncodedPattern(stack); + } + } +} + diff --git a/src/main/java/com/extendedae_plus/util/uploadPattern/ExtendedAEPatternUploadUtil.java b/src/main/java/com/extendedae_plus/util/uploadPattern/ExtendedAEPatternUploadUtil.java index 154e3b4..a56d204 100644 --- a/src/main/java/com/extendedae_plus/util/uploadPattern/ExtendedAEPatternUploadUtil.java +++ b/src/main/java/com/extendedae_plus/util/uploadPattern/ExtendedAEPatternUploadUtil.java @@ -517,6 +517,67 @@ public class ExtendedAEPatternUploadUtil { return false; } + /** + * 将给定的已编码样板直接上传到装配矩阵(不依赖编码终端槽位)。 + * + * @param player 服务器玩家 + * @param pattern 已编码样板 + * @param grid AE 网络 + * @return 是否成功插入矩阵 + */ + public static boolean uploadPatternToMatrix(ServerPlayer player, ItemStack pattern, IGrid grid) { + if (player == null || grid == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) { + return false; + } + + IPatternDetails details = PatternDetailsHelper.decodePattern(pattern, player.level()); + if (!(details instanceof AECraftingPattern + || details instanceof AESmithingTablePattern + || details instanceof AEStonecuttingPattern)) { + sendMessage(player, "extendedae_plus.upload_to_matrix.fail"); + return false; + } + + if (matrixContainsPattern(grid, pattern)) { + if (player != null) { + player.sendSystemMessage(Component.translatable("extendedae_plus.message.matrix.duplicate")); + } + return false; + } + + List inventories = findAllMatrixPatternInventories(grid); + if (!inventories.isEmpty()) { + for (InternalInventory inv : inventories) { + if (inv == null) continue; + ItemStack toInsert = pattern.copy(); + ItemStack remain = inv.addItems(toInsert); + if (remain.getCount() < toInsert.getCount()) { + sendMessage(player, "extendedae_plus.upload_to_matrix.success"); + return true; + } + } + } + + List handlers = findAllMatrixPatternHandlers(grid); + if (!handlers.isEmpty()) { + for (Object cap : handlers) { + ItemStack toInsert = pattern.copy(); + ItemStack remain = insertIntoAnySlot(cap, toInsert); + if (remain.getCount() < toInsert.getCount()) { + sendMessage(player, "extendedae_plus.upload_to_matrix.success"); + return true; + } + } + } + + if (inventories.isEmpty() && handlers.isEmpty()) { + sendMessage(player, "extendedae_plus.upload_to_matrix.fail_no_matrix"); + } else { + sendMessage(player, "extendedae_plus.upload_to_matrix.fail_full"); + } + return false; + } + /** * 在给定 AE Grid 中收集所有已成型且在线的装配矩阵“图样模块”的用于外部插入的内部库存。 * 优先使用 TileAssemblerMatrixPattern#getExposedInventory(仅允许插入,且已带AE过滤规则)。 diff --git a/src/main/resources/assets/extendedae_plus/lang/en_us.json b/src/main/resources/assets/extendedae_plus/lang/en_us.json index 2ca952e..5f827e8 100644 --- a/src/main/resources/assets/extendedae_plus/lang/en_us.json +++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json @@ -61,6 +61,14 @@ "extendedae_plus.screen.remove_mapping": "Remove Mapping", "extendedae_plus.screen.cn_name": "Chinese Name", "extendedae_plus.button.choose_provider": "Upload Pattern", + "key.extendedae_plus.create_pattern": "Create Pattern (Ctrl+Q)", + "key.categories.extendedae_plus": "ExtendedAE Plus", + "message.extendedae_plus.hover_item_first": "Hover an item first.", + "message.extendedae_plus.no_recipes_found": "No related recipes found.", + "message.extendedae_plus.recipe_not_found": "Recipe not found.", + "message.extendedae_plus.no_blank_pattern": "No blank pattern available.", + "message.extendedae_plus.pattern_creation_failed": "Failed to create encoded pattern.", + "message.extendedae_plus.no_network": "Unable to access AE network.", "extendedae_plus.pattern.hovertext.player": "Encoded by %s", "item.extendedae_plus.entity_speed_ticker": "Entity Accelerator", @@ -231,4 +239,4 @@ "extendedae_plus.jade.network.offline": "Offline", "extendedae_plus.screen.global_controller_title": "Pattern Provider Status Controller" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json index 44e17d0..6d563cc 100644 --- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json +++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json @@ -61,6 +61,14 @@ "extendedae_plus.screen.remove_mapping": "删除映射", "extendedae_plus.screen.cn_name": "中文名", "extendedae_plus.button.choose_provider":"上传样板", + "key.extendedae_plus.create_pattern": "快速创建样板 (Ctrl+Q)", + "key.categories.extendedae_plus": "扩展AE Plus", + "message.extendedae_plus.hover_item_first": "请先将鼠标悬停在物品上。", + "message.extendedae_plus.no_recipes_found": "未找到相关配方。", + "message.extendedae_plus.recipe_not_found": "未找到该配方。", + "message.extendedae_plus.no_blank_pattern": "没有可用空白样板。", + "message.extendedae_plus.pattern_creation_failed": "创建编码样板失败。", + "message.extendedae_plus.no_network": "无法访问 AE 网络。", "extendedae_plus.pattern.hovertext.player": "由 %s 编码", "item.extendedae_plus.entity_speed_ticker": "实体加速器", @@ -226,4 +234,4 @@ "extendedae_plus.jade.network.offline": "设备离线", "extendedae_plus.screen.global_controller_title": "样板供应器状态控制器" -} \ No newline at end of file +}