From 7c2286cd688ff7e5d4e0f3de9ea665e050a318a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=8C=83=E7=BF=B0=E5=BE=B7202509?= <2390616704@qq.com>
Date: Sun, 22 Feb 2026 11:13:35 +0800
Subject: [PATCH] feat: Add Ctrl+Q quick pattern creation from JEI
Implement a new keybind (Ctrl+Q) to quickly create AE2 patterns from JEI items.
Features:
- Press Ctrl+Q while hovering over any item in JEI to create patterns instantly
- Intelligent recipe selection (prioritizes crafting recipes)
- Support Shift+Ctrl+Q to search recipes where item is used as input
- JEI bookmark priority system for ingredient selection
- Smart pattern placement (crafting patterns drop, processing patterns to inventory)
- Consumes blank pattern from player inventory with auto-refund on failure
Configuration:
- ctrlQConsumeBlankPattern: Toggle blank pattern consumption (default: true)
- ctrlQExtractFromNetwork: Extract patterns from AE network (default: true, WIP)
Technical changes:
- Add CtrlQPatternKeyHandler for client-side key handling
- Add CreateCtrlQPatternC2SPacket for C2S network communication
- Add RecipeFinderUtil with priority sorting
- Add ModKeybindings for keybind registration
- Update ModConfig with new options
- Register network packet in ModNetwork
- Add English translations
Code quality:
- Cleaned up debug logs
- Removed local development configuration
---
gradle.properties | 3 +-
.../client/ModKeybindings.java | 33 +++
.../client/event/CtrlQPatternKeyHandler.java | 184 ++++++++++++++
.../com/extendedae_plus/config/ModConfig.java | 21 ++
.../com/extendedae_plus/init/ModNetwork.java | 7 +
.../pattern/CreateCtrlQPatternC2SPacket.java | 228 ++++++++++++++++++
.../util/RecipeFinderUtil.java | 134 ++++++++++
.../assets/extendedae_plus/lang/en_us.json | 12 +-
.../assets/extendedae_plus/lang/zh_cn.json | 12 +-
9 files changed, 631 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/pattern/CreateCtrlQPatternC2SPacket.java
create mode 100644 src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java
diff --git a/gradle.properties b/gradle.properties
index 4ed81b7..d1e9557 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,8 +1,8 @@
# Done to increase the memory available to Gradle.
org.gradle.jvmargs=-Xmx1G
loom.platform = forge
+org.gradle.parallel=true
-# Mod properties
mod_version = 1.5.1
maven_group = com.extendedae_plus
archives_name = extendedae_plus
@@ -31,3 +31,4 @@ ldlib_version=5394816
ie_version=5224387
mixin_version=0.8.4
curios_version=6418456
+org.gradle.parallel=true
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..4e4cbc2
--- /dev/null
+++ b/src/main/java/com/extendedae_plus/client/ModKeybindings.java
@@ -0,0 +1,33 @@
+package com.extendedae_plus.client;
+
+import com.mojang.blaze3d.platform.InputConstants;
+import net.minecraft.client.KeyMapping;
+import net.minecraftforge.client.settings.KeyConflictContext;
+import org.lwjgl.glfw.GLFW;
+
+/**
+ * ExtendedAE Plus 快捷键定义
+ */
+public final class ModKeybindings {
+ private ModKeybindings() {}
+
+ /**
+ * Ctrl+Q 快速创建样板快捷键
+ */
+ public static final KeyMapping CREATE_PATTERN_KEY = new KeyMapping(
+ "key.extendedae_plus.create_pattern", // 翻译键
+ KeyConflictContext.GUI, // 仅在GUI中生效
+ InputConstants.Type.KEYSYM, // 键盘按键类型
+ GLFW.GLFW_KEY_Q, // Q 键
+ "key.categories.extendedae_plus" // 分类
+ );
+
+ /**
+ * 注册所有快捷键
+ *
+ * @param event Forge快捷键注册事件
+ */
+ public static void register(net.minecraftforge.client.event.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..cfaff46
--- /dev/null
+++ b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java
@@ -0,0 +1,184 @@
+package com.extendedae_plus.client.event;
+
+import appeng.api.stacks.AEItemKey;
+import appeng.api.stacks.AEKey;
+import com.extendedae_plus.ExtendedAEPlus;
+import com.extendedae_plus.client.ModKeybindings;
+import com.extendedae_plus.init.ModNetwork;
+import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
+import com.extendedae_plus.network.pattern.CreateCtrlQPatternC2SPacket;
+import com.extendedae_plus.util.RecipeFinderUtil;
+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.world.item.ItemStack;
+import net.minecraft.world.item.crafting.CraftingRecipe;
+import net.minecraft.world.item.crafting.Ingredient;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraftforge.api.distmarker.Dist;
+import net.minecraftforge.client.event.ScreenEvent;
+import net.minecraftforge.eventbus.api.SubscribeEvent;
+import net.minecraftforge.fml.common.Mod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Ctrl+Q键快速创建样板事件监听器
+ *
+ *
监听 Ctrl+Q 组合键,自动创建样板并掉落到玩家脚下
+ * 应用 JEI 书签优先级选择材料,优先选择工作台配方
+ */
+@Mod.EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT)
+public class CtrlQPatternKeyHandler {
+ private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - CtrlQKeyHandler");
+
+ @SubscribeEvent
+ public static void onScreenKeyPressed(ScreenEvent.KeyPressed event) {
+ Screen screen = event.getScreen();
+ int keyCode = event.getKeyCode();
+ int scanCode = event.getScanCode();
+
+ // 使用 KeyMapping 检测按键(而非硬编码)
+ if (!ModKeybindings.CREATE_PATTERN_KEY.matches(keyCode, scanCode)) {
+ return;
+ }
+
+ // 检查 Ctrl 修饰键
+ if (!Screen.hasControlDown()) {
+ return;
+ }
+
+ // JEI 必须可用
+ if (JeiRuntimeProxy.get() == null) {
+ LOGGER.warn("[CtrlQKeyHandler] JEI not available");
+ return;
+ }
+
+ // 获取鼠标悬浮的物品
+ Optional> ingredient = JeiRuntimeProxy.getIngredientUnderMouse();
+
+ if (ingredient.isEmpty()) {
+ LOGGER.warn("[CtrlQKeyHandler] No ingredient under mouse");
+ Minecraft mc = Minecraft.getInstance();
+ if (mc.player != null) {
+ mc.player.displayClientMessage(
+ Component.translatable("message.extendedae_plus.hover_item_first"),
+ true
+ );
+ }
+ return;
+ }
+
+ // 查找相关配方
+ Minecraft mc = Minecraft.getInstance();
+ List> recipes = RecipeFinderUtil.findRecipesByIngredient(
+ ingredient.get(),
+ mc.level
+ );
+
+ if (recipes.isEmpty()) {
+ LOGGER.warn("[CtrlQKeyHandler] No recipes found");
+ if (mc.player != null) {
+ mc.player.displayClientMessage(
+ Component.translatable("message.extendedae_plus.no_recipes_found"),
+ true
+ );
+ }
+ return;
+ }
+
+ // 自动选择最佳配方(优先CraftingRecipe)
+ Recipe> selectedRecipe = RecipeFinderUtil.selectBestRecipe(recipes);
+ if (selectedRecipe == null) {
+ LOGGER.error("[CtrlQKeyHandler] selectBestRecipe returned null");
+ return;
+ }
+
+ boolean isCraftingPattern = selectedRecipe instanceof CraftingRecipe;
+
+ // 应用JEI书签优先级选择材料
+ List selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipe);
+
+ // 发送网络包到服务器
+ ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket(
+ selectedRecipe.getId(),
+ isCraftingPattern,
+ selectedIngredients
+ ));
+
+ // 消耗事件,防止传播
+ event.setCanceled(true);
+ }
+
+ /**
+ * 应用JEI书签优先级选择配方材料
+ *
+ * 对配方的每个 Ingredient,选择 JEI 书签中优先级最高的物品
+ * 如果没有在书签中,则使用配方默认的第一个物品
+ *
+ * @param recipe 配方
+ * @return 选择的材料列表
+ */
+ private static List selectIngredientsWithJeiPriority(Recipe> recipe) {
+ // 获取JEI书签列表并构建优先级映射
+ List extends ITypedIngredient>> bookmarks = JeiRuntimeProxy.getBookmarkList();
+ Map priorities = new HashMap<>();
+ AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE);
+
+ // 构建优先级映射 (数值越小 = 优先级越高,与EncodingHelperMixin逻辑一致)
+ for (ITypedIngredient> ingredient : bookmarks) {
+ ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack ->
+ priorities.put(AEItemKey.of(itemStack), index.getAndDecrement())
+ );
+ }
+
+ List selected = new ArrayList<>();
+
+ // 对每个 ingredient 选择优先级最高的物品
+ for (Ingredient ingredient : recipe.getIngredients()) {
+ if (ingredient.isEmpty()) {
+ selected.add(ItemStack.EMPTY);
+ continue;
+ }
+
+ ItemStack[] items = ingredient.getItems();
+ if (items.length == 0) {
+ selected.add(ItemStack.EMPTY);
+ continue;
+ }
+
+ // 选择优先级最高的 (如果都不在书签中,选第一个)
+ ItemStack best = items[0];
+ int bestPriority = Integer.MAX_VALUE;
+
+ // 检查第一个物品的优先级
+ AEKey firstKey = AEItemKey.of(best);
+ if (priorities.containsKey(firstKey)) {
+ bestPriority = priorities.get(firstKey);
+ }
+
+ // 遍历其他选项
+ for (int i = 1; i < items.length; i++) {
+ AEKey key = AEItemKey.of(items[i]);
+ int priority = priorities.getOrDefault(key, Integer.MAX_VALUE);
+ if (priority < bestPriority) {
+ bestPriority = priority;
+ best = items[i];
+ }
+ }
+
+ selected.add(best.copy());
+ }
+
+ return selected;
+ }
+}
diff --git a/src/main/java/com/extendedae_plus/config/ModConfig.java b/src/main/java/com/extendedae_plus/config/ModConfig.java
index 8877728..0b387d9 100644
--- a/src/main/java/com/extendedae_plus/config/ModConfig.java
+++ b/src/main/java/com/extendedae_plus/config/ModConfig.java
@@ -170,4 +170,25 @@ public final class ModConfig {
ConfigParsingUtils.reload();
}
}
+
+ // ==================== Ctrl+Q 快速样板配置 ====================
+
+ @Configurable
+ @Configurable.Comment(value = {
+ "Ctrl+Q创建样板是否消耗空白样板",
+ "true: 从玩家背包或AE网络消耗空白样板",
+ "false: 不消耗空白样板,直接创建(整合包/服务器管理员可配置)"
+ })
+ @Configurable.Synchronized
+ public boolean ctrlQConsumeBlankPattern = true;
+
+ @Configurable
+ @Configurable.Comment(value = {
+ "Ctrl+Q创建样板是否优先从AE网络提取空白样板",
+ "true: 优先从AE网络提取,网络无货才从背包消耗",
+ "false: 仅从玩家背包消耗",
+ "注意:需要玩家持有或装备无线终端才能访问AE网络"
+ })
+ @Configurable.Synchronized
+ public boolean ctrlQExtractFromNetwork = true;
}
\ No newline at end of file
diff --git a/src/main/java/com/extendedae_plus/init/ModNetwork.java b/src/main/java/com/extendedae_plus/init/ModNetwork.java
index dc20543..ebbc3d2 100644
--- a/src/main/java/com/extendedae_plus/init/ModNetwork.java
+++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java
@@ -6,6 +6,7 @@ import com.extendedae_plus.network.crafting.CraftingMonitorJumpC2SPacket;
import com.extendedae_plus.network.crafting.CraftingMonitorOpenProviderC2SPacket;
import com.extendedae_plus.network.crafting.OpenCraftFromJeiC2SPacket;
import com.extendedae_plus.network.meInterface.InterfaceAdjustConfigAmountC2SPacket;
+import com.extendedae_plus.network.pattern.CreateCtrlQPatternC2SPacket;
import com.extendedae_plus.network.provider.*;
import com.extendedae_plus.network.upload.EncodeWithShiftFlagC2SPacket;
import net.minecraft.resources.ResourceLocation;
@@ -168,6 +169,12 @@ public final class ModNetwork {
.decoder(LabelNetworkListS2CPacket::decode)
.consumerNetworkThread(LabelNetworkListS2CPacket::handle)
.add();
+
+ CHANNEL.messageBuilder(CreateCtrlQPatternC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
+ .encoder(CreateCtrlQPatternC2SPacket::encode)
+ .decoder(CreateCtrlQPatternC2SPacket::decode)
+ .consumerNetworkThread(CreateCtrlQPatternC2SPacket::handle)
+ .add();
}
private static int nextId() { return id++; }
diff --git a/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java b/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java
new file mode 100644
index 0000000..c0e2224
--- /dev/null
+++ b/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java
@@ -0,0 +1,228 @@
+package com.extendedae_plus.network.pattern;
+
+import appeng.api.crafting.PatternDetailsHelper;
+import appeng.api.stacks.AEItemKey;
+import appeng.api.stacks.GenericStack;
+import appeng.core.definitions.AEItems;
+import net.minecraft.network.FriendlyByteBuf;
+import net.minecraft.network.chat.Component;
+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.crafting.CraftingRecipe;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraft.world.item.crafting.RecipeManager;
+import net.minecraftforge.network.NetworkEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Supplier;
+
+/**
+ * C2S: Ctrl+Q快速创建样板数据包
+ *
+ * 从客户端发送配方ID和选择的材料到服务器,服务器消耗空白样板并创建编码样板掉落到玩家脚下
+ */
+public class CreateCtrlQPatternC2SPacket {
+ private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - CtrlQPattern");
+
+ private final ResourceLocation recipeId;
+ private final boolean isCraftingPattern;
+ private final List selectedIngredients;
+
+ public CreateCtrlQPatternC2SPacket(ResourceLocation recipeId, boolean isCraftingPattern, List selectedIngredients) {
+ this.recipeId = recipeId;
+ this.isCraftingPattern = isCraftingPattern;
+ this.selectedIngredients = selectedIngredients;
+ }
+
+ public static void encode(CreateCtrlQPatternC2SPacket msg, FriendlyByteBuf buf) {
+ buf.writeResourceLocation(msg.recipeId);
+ buf.writeBoolean(msg.isCraftingPattern);
+ buf.writeInt(msg.selectedIngredients.size());
+ for (ItemStack stack : msg.selectedIngredients) {
+ buf.writeItem(stack);
+ }
+ }
+
+ public static CreateCtrlQPatternC2SPacket decode(FriendlyByteBuf buf) {
+ ResourceLocation recipeId = buf.readResourceLocation();
+ boolean isCraftingPattern = buf.readBoolean();
+ int count = buf.readInt();
+ List ingredients = new ArrayList<>();
+ for (int i = 0; i < count; i++) {
+ ingredients.add(buf.readItem());
+ }
+ return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients);
+ }
+
+ public static void handle(CreateCtrlQPatternC2SPacket msg, Supplier ctxSupplier) {
+ var ctx = ctxSupplier.get();
+ ctx.enqueueWork(() -> {
+ ServerPlayer player = ctx.getSender();
+ if (player == null) {
+ LOGGER.warn("[CtrlQPattern] No sender found");
+ return;
+ }
+
+
+ // 1. 验证配方存在
+ RecipeManager recipeManager = player.level().getRecipeManager();
+ var recipeOpt = recipeManager.byKey(msg.recipeId);
+
+ if (recipeOpt.isEmpty()) {
+ LOGGER.error("[CtrlQPattern] Recipe not found: {}", msg.recipeId);
+ player.displayClientMessage(
+ Component.translatable("message.extendedae_plus.recipe_not_found"),
+ false
+ );
+ return;
+ }
+
+ Recipe> recipe = recipeOpt.get();
+
+ // 2. 消耗空白样板
+ if (!consumeBlankPattern(player)) {
+ LOGGER.warn("[CtrlQPattern] No blank pattern found in inventory");
+ player.displayClientMessage(
+ Component.translatable("message.extendedae_plus.no_blank_pattern"),
+ false
+ );
+ return;
+ }
+
+ // 3. 创建样板
+ ItemStack pattern = createPattern(recipe, msg.isCraftingPattern, msg.selectedIngredients, player);
+
+ if (pattern.isEmpty()) {
+ LOGGER.error("[CtrlQPattern] Pattern creation failed");
+ // 创建失败,退还空白样板
+ player.getInventory().add(AEItems.BLANK_PATTERN.stack());
+ player.displayClientMessage(
+ Component.translatable("message.extendedae_plus.pattern_creation_failed"),
+ false
+ );
+ return;
+ }
+
+ // 4. 根据样板类型选择交付方式
+ if (msg.isCraftingPattern) {
+ // 合成样板:始终掉落到玩家脚下
+ player.drop(pattern, false);
+ } else {
+ // 处理样板:优先放入背包,满了再掉落
+ boolean added = player.getInventory().add(pattern);
+ if (added) {
+ } else {
+ player.drop(pattern, false);
+ }
+ }
+
+ // 5. 移除成功消息(仅失败时提示)
+ });
+ ctx.setPacketHandled(true);
+ }
+
+ /**
+ * 消耗玩家背包中的一个空白样板
+ *
+ * @param player 玩家
+ * @return 是否成功消耗
+ */
+ private static boolean consumeBlankPattern(ServerPlayer player) {
+ Inventory inventory = player.getInventory();
+
+ // 遍历背包查找空白样板
+ for (int i = 0; i < inventory.getContainerSize(); i++) {
+ ItemStack stack = inventory.getItem(i);
+ if (stack.is(AEItems.BLANK_PATTERN.asItem())) {
+ stack.shrink(1); // 消耗一个
+ return true;
+ }
+ }
+
+ return false; // 未找到
+ }
+
+ /**
+ * 从配方创建样板
+ *
+ * @param recipe 配方
+ * @param isCrafting 是否为合成样板
+ * @param selectedIngredients 客户端选择的材料(应用JEI优先级后)
+ * @param player 玩家
+ * @return 编码的样板物品
+ */
+ private static ItemStack createPattern(Recipe> recipe, boolean isCrafting, List selectedIngredients, ServerPlayer player) {
+ try {
+ if (isCrafting && recipe instanceof CraftingRecipe craftingRecipe) {
+ // ===== 合成样板创建路径 =====
+
+ // 准备9格工作台输入(3x3布局)
+ 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 = recipe.getResultItem(player.level().registryAccess()).copy();
+
+ // 使用 encodeCraftingPattern 创建合成样板
+ // 直接传递 CraftingRecipe 对象而非 RecipeHolder
+ ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern(
+ craftingRecipe,
+ inputs,
+ output,
+ true, // allowSubstitutes - 允许替代材料
+ false // allowFluidSubstitutes - 不允许流体替代
+ );
+
+ return encodedPattern;
+
+ } else {
+ // ===== 处理样板创建路径 =====
+
+ List inputs = new ArrayList<>();
+ List outputs = new ArrayList<>();
+
+ // 处理输入 - 使用客户端传入的材料选择
+ for (ItemStack item : selectedIngredients) {
+ if (!item.isEmpty()) {
+ inputs.add(new GenericStack(
+ AEItemKey.of(item),
+ item.getCount()
+ ));
+ }
+ }
+
+ // 处理输出
+ ItemStack result = recipe.getResultItem(player.level().registryAccess());
+ if (!result.isEmpty()) {
+ outputs.add(new GenericStack(
+ AEItemKey.of(result),
+ result.getCount()
+ ));
+ }
+
+ // 使用 encodeProcessingPattern 创建处理样板
+ ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(
+ inputs.toArray(new GenericStack[0]),
+ outputs.toArray(new GenericStack[0])
+ );
+
+ return encodedPattern;
+ }
+
+ } catch (Exception e) {
+ LOGGER.error("[CtrlQPattern] Exception during pattern creation", e);
+ return ItemStack.EMPTY;
+ }
+ }
+}
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..cc38327
--- /dev/null
+++ b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java
@@ -0,0 +1,134 @@
+package com.extendedae_plus.util;
+
+import mezz.jei.api.constants.VanillaTypes;
+import mezz.jei.api.ingredients.ITypedIngredient;
+import net.minecraft.client.gui.screens.Screen;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.crafting.CraftingRecipe;
+import net.minecraft.world.item.crafting.Recipe;
+import net.minecraft.world.level.Level;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 配方查找工具类
+ *
+ * 根据物品查找相关配方,优先返回工作台配方(CraftingRecipe)
+ */
+public class RecipeFinderUtil {
+ private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder");
+
+ /**
+ * 根据JEI物品查找相关配方
+ *
+ * @param ingredient JEI物品
+ * @param level 当前世界
+ * @return 相关配方列表
+ */
+ public static List> findRecipesByIngredient(ITypedIngredient> ingredient, Level level) {
+ if (ingredient.getType() == VanillaTypes.ITEM_STACK) {
+ ItemStack stack = (ItemStack) ingredient.getIngredient();
+ return findRecipesByItem(stack, level);
+ }
+
+ LOGGER.warn("[RecipeFinder] Unsupported ingredient type: {}", ingredient.getType());
+ // TODO: Support fluids, chemicals, and other AE2-compatible types
+ return List.of();
+ }
+
+ /**
+ * 根据物品查找相关配方
+ *
+ * @param item 目标物品
+ * @param level 当前世界
+ * @return 配方列表
+ */
+ private static List> findRecipesByItem(ItemStack item, Level level) {
+ List> results = new ArrayList<>();
+ int totalRecipes = level.getRecipeManager().getRecipes().size();
+
+ // 1. 查找以该物品为输出的配方
+ int outputMatches = 0;
+ for (Recipe> recipe : level.getRecipeManager().getRecipes()) {
+ if (matchesOutput(recipe, item)) {
+ results.add(recipe);
+ outputMatches++;
+ }
+ }
+
+ // 2. 如果按住Shift,也查找以该物品为输入的配方
+ if (Screen.hasShiftDown()) {
+ int inputMatches = 0;
+ for (Recipe> recipe : level.getRecipeManager().getRecipes()) {
+ if (matchesInput(recipe, item) && !results.contains(recipe)) {
+ results.add(recipe);
+ inputMatches++;
+ }
+ }
+ }
+
+ // 3. 优先级排序: CraftingRecipe优先
+ results.sort((r1, r2) -> {
+ boolean isCrafting1 = r1 instanceof CraftingRecipe;
+ boolean isCrafting2 = r2 instanceof CraftingRecipe;
+ if (isCrafting1 && !isCrafting2) return -1; // r1优先
+ if (!isCrafting1 && isCrafting2) return 1; // r2优先
+ return 0; // 保持原顺序
+ });
+
+ return results;
+ }
+
+ /**
+ * 选择最佳配方(优先选择工作台配方)
+ *
+ * @param recipes 配方列表
+ * @return 最佳配方,如果列表为空返回null
+ */
+ public static Recipe> selectBestRecipe(List> recipes) {
+ if (recipes.isEmpty()) {
+ return null;
+ }
+
+ // 优先返回CraftingRecipe
+ for (Recipe> recipe : recipes) {
+ if (recipe instanceof CraftingRecipe) {
+ return recipe;
+ }
+ }
+
+ // 没有工作台配方,返回第一个
+ return recipes.get(0);
+ }
+
+ /**
+ * 检查配方输出是否匹配目标物品
+ */
+ private static boolean matchesOutput(Recipe> recipe, ItemStack target) {
+ try {
+ ItemStack result = recipe.getResultItem(null);
+ boolean matches = ItemStack.isSameItemSameTags(result, target);
+ return matches;
+ } catch (Exception e) {
+ LOGGER.warn("[RecipeFinder] Exception in matchesOutput for recipe {}: {}", recipe.getId(), e.getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * 检查配方输入是否包含目标物品
+ */
+ private static boolean matchesInput(Recipe> recipe, ItemStack target) {
+ try {
+ boolean matches = recipe.getIngredients().stream()
+ .anyMatch(ingredient -> ingredient.test(target));
+ return matches;
+ } catch (Exception e) {
+ LOGGER.warn("[RecipeFinder] Exception in matchesInput for recipe {}: {}", recipe.getId(), e.getMessage());
+ return false;
+ }
+ }
+}
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 6081246..ecd0b5f 100644
--- a/src/main/resources/assets/extendedae_plus/lang/en_us.json
+++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json
@@ -227,5 +227,15 @@
"extendedae_plus.command.server_side_only": "This command must be run on server side",
"extendedae_plus.command.storage_manager_not_initialized": "InfinityStorageManager is not initialized",
"extendedae_plus.command.gave_infinity_disks": "Gave %s infinity disks",
- "extendedae_plus.command.error": "Error: %s"
+ "extendedae_plus.command.error": "Error: %s",
+
+ "message.extendedae_plus.hover_item_first": "Please hover over an item first",
+ "message.extendedae_plus.no_recipes_found": "No recipes found for this item",
+ "message.extendedae_plus.no_blank_pattern": "No blank pattern in inventory",
+ "message.extendedae_plus.recipe_not_found": "Recipe not found",
+ "message.extendedae_plus.pattern_creation_failed": "Pattern creation failed",
+ "message.extendedae_plus.pattern_created": "Created pattern: %s",
+
+ "key.extendedae_plus.create_pattern": "Create Pattern from JEI",
+ "key.categories.extendedae_plus": "ExtendedAE Plus"
}
\ 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 64acdea..9a94431 100644
--- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json
+++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json
@@ -226,5 +226,15 @@
"extendedae_plus.command.server_side_only": "此命令必须在服务器端执行",
"extendedae_plus.command.storage_manager_not_initialized": "InfinityStorageManager未初始化",
"extendedae_plus.command.gave_infinity_disks": "已发放 %s 个无限磁盘",
- "extendedae_plus.command.error": "错误: %s"
+ "extendedae_plus.command.error": "错误: %s",
+
+ "message.extendedae_plus.hover_item_first": "请先将鼠标悬浮在物品上",
+ "message.extendedae_plus.no_recipes_found": "未找到该物品的配方",
+ "message.extendedae_plus.no_blank_pattern": "背包中没有空白样板",
+ "message.extendedae_plus.recipe_not_found": "配方未找到",
+ "message.extendedae_plus.pattern_creation_failed": "样板创建失败",
+ "message.extendedae_plus.pattern_created": "已创建样板: %s",
+
+ "key.extendedae_plus.create_pattern": "从JEI创建样板",
+ "key.categories.extendedae_plus": "ExtendedAE Plus"
}
\ No newline at end of file