diff --git a/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java index 8bb097c..6867d86 100644 --- a/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java +++ b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java @@ -1,28 +1,24 @@ 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 com.extendedae_plus.util.RecipeInfo; 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.Item; 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 java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -68,11 +64,10 @@ public class CtrlQPatternKeyHandler { return; } - // 查找相关配方 + // 查找相关配方(使用新的 API,包含完整数量信息) Minecraft mc = Minecraft.getInstance(); - List> recipes = RecipeFinderUtil.findRecipesByIngredient( - ingredient.get(), - mc.level + List recipes = RecipeFinderUtil.findRecipesByIngredient( + ingredient.get() ); if (recipes.isEmpty()) { @@ -85,21 +80,19 @@ public class CtrlQPatternKeyHandler { return; } - // 自动选择最佳配方(优先CraftingRecipe) - Recipe selectedRecipe = RecipeFinderUtil.selectBestRecipe(recipes); - if (selectedRecipe == null) { + // 自动选择最佳配方(优先工作台配方) + RecipeInfo selectedRecipeInfo = RecipeFinderUtil.selectBestRecipe(recipes); + if (selectedRecipeInfo == null) { return; } - boolean isCraftingPattern = selectedRecipe instanceof CraftingRecipe; - // 应用JEI书签优先级选择材料 - List selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipe); + List selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipeInfo); // 发送网络包到服务器 ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket( - selectedRecipe.getId(), - isCraftingPattern, + selectedRecipeInfo.getRecipe().getId(), + selectedRecipeInfo.isCraftingRecipe(), selectedIngredients )); @@ -110,63 +103,26 @@ public class CtrlQPatternKeyHandler { /** * 应用JEI书签优先级选择配方材料 * - *

对配方的每个 Ingredient,选择 JEI 书签中优先级最高的物品

- *

如果没有在书签中,则使用配方默认的第一个物品

+ *

对配方的每个输入槽位,选择 JEI 书签中优先级最高的物品

+ *

如果没有在书签中,则使用槽位的第一个物品

* - * @param recipe 配方 + * @param recipeInfo 配方信息(包含完整的输入输出数量) * @return 选择的材料列表 */ - private static List selectIngredientsWithJeiPriority(Recipe recipe) { + private static List selectIngredientsWithJeiPriority(RecipeInfo recipeInfo) { // 获取JEI书签列表并构建优先级映射 List> bookmarks = JeiRuntimeProxy.getBookmarkList(); - Map priorities = new HashMap<>(); + 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()) + priorities.put(itemStack.getItem(), 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; + // 使用 RecipeInfo 的方法选择最佳输入 + return recipeInfo.selectBestInputs(priorities); } } diff --git a/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java index daa0fb9..81cc640 100644 --- a/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java +++ b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java @@ -1,130 +1,210 @@ package com.extendedae_plus.util; +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 net.minecraft.client.gui.screens.Screen; +import mezz.jei.api.recipe.IFocus; +import mezz.jei.api.recipe.IFocusFactory; +import mezz.jei.api.recipe.IRecipeManager; +import mezz.jei.api.recipe.RecipeIngredientRole; +import mezz.jei.api.recipe.category.IRecipeCategory; +import mezz.jei.api.runtime.IJeiRuntime; 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; +import java.util.Optional; /** * 配方查找工具类 * - *

根据物品查找相关配方,优先返回工作台配方(CraftingRecipe)

+ *

使用 JEI API 根据物品查找相关配方,返回包含完整数量信息的 RecipeInfo

*/ public class RecipeFinderUtil { private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder"); /** - * 根据JEI物品查找相关配方 + * 根据JEI物品查找相关配方(仅搜索以该物品为输出的配方) * * @param ingredient JEI物品 - * @param level 当前世界 - * @return 相关配方列表 + * @return 相关配方信息列表(包含完整的输入输出数量) */ - public static List> findRecipesByIngredient(ITypedIngredient ingredient, Level level) { - if (ingredient.getType() == VanillaTypes.ITEM_STACK) { - ItemStack stack = (ItemStack) ingredient.getIngredient(); - return findRecipesByItem(stack, level); + public static List findRecipesByIngredient(ITypedIngredient ingredient) { + // 获取 JEI Runtime + IJeiRuntime jeiRuntime = JeiRuntimeProxy.get(); + if (jeiRuntime == null) { + LOGGER.warn("[RecipeFinder] JEI Runtime not available"); + return List.of(); } - 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<>(); - - // 1. 查找以该物品为输出的配方 - for (Recipe recipe : level.getRecipeManager().getRecipes()) { - if (matchesOutput(recipe, item, level)) { - results.add(recipe); - } + // 只支持物品类型 + if (ingredient.getType() != VanillaTypes.ITEM_STACK) { + LOGGER.warn("[RecipeFinder] Unsupported ingredient type: {}", ingredient.getType()); + // TODO: Support fluids, chemicals, and other AE2-compatible types + return List.of(); } - // 2. 如果按住Shift,也查找以该物品为输入的配方 - if (Screen.hasShiftDown()) { - for (Recipe recipe : level.getRecipeManager().getRecipes()) { - if (matchesInput(recipe, item) && !results.contains(recipe)) { - results.add(recipe); + IJeiHelpers jeiHelpers = jeiRuntime.getJeiHelpers(); + IRecipeManager recipeManager = jeiRuntime.getRecipeManager(); + IFocusFactory focusFactory = jeiHelpers.getFocusFactory(); + + // 创建输出焦点(OUTPUT role) + IFocus outputFocus = focusFactory.createFocus( + RecipeIngredientRole.OUTPUT, + ingredient + ); + + List results = new ArrayList<>(); + + // 查找工作台配方 + try { + IRecipeCategory craftingCategory = recipeManager.getRecipeCategory(RecipeTypes.CRAFTING); + + recipeManager.createRecipeLookup(RecipeTypes.CRAFTING) + .limitFocus(List.of(outputFocus)) + .get() + .forEach(recipe -> { + // 创建配方布局以获取完整信息 + Optional> layoutOpt = + recipeManager.createRecipeLayoutDrawable( + craftingCategory, + recipe, + focusFactory.getEmptyFocusGroup() + ); + + layoutOpt.ifPresent(layout -> { + RecipeInfo info = extractRecipeInfo(recipe, layout, true); + if (info != null) { + results.add(info); + } + }); + }); + } catch (Exception e) { + LOGGER.warn("[RecipeFinder] Error searching crafting recipes: {}", e.getMessage()); + } + + // 查找其他所有配方类型(排除工作台配方) + try { + jeiHelpers.getAllRecipeTypes().forEach(recipeType -> { + // 跳过工作台配方(已经处理过) + if (recipeType.equals(RecipeTypes.CRAFTING)) { + return; } - } + + try { + @SuppressWarnings("unchecked") + IRecipeCategory> category = (IRecipeCategory>) recipeManager.getRecipeCategory(recipeType); + + recipeManager.createRecipeLookup(recipeType) + .limitFocus(List.of(outputFocus)) + .get() + .forEach(recipe -> { + if (recipe instanceof Recipe rawRecipe) { + // 创建配方布局以获取完整信息 + Optional>> layoutOpt = + recipeManager.createRecipeLayoutDrawable( + category, + rawRecipe, + focusFactory.getEmptyFocusGroup() + ); + + layoutOpt.ifPresent(layout -> { + RecipeInfo info = extractRecipeInfo(rawRecipe, layout, false); + if (info != null) { + results.add(info); + } + }); + } + }); + } catch (Exception e) { + // 某些配方类型可能不支持,静默忽略 + } + }); + } catch (Exception e) { + LOGGER.warn("[RecipeFinder] Error searching other recipe types: {}", e.getMessage()); } - // 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; // 保持原顺序 - }); + LOGGER.debug("[RecipeFinder] Found {} recipes for output: {}", + results.size(), + ((ItemStack) ingredient.getIngredient()).getDescriptionId()); return results; } + /** + * 从配方布局中提取完整的配方信息 + * + * @param recipe 原始配方对象 + * @param layout JEI 配方布局(包含完整的槽位和数量信息) + * @param isCrafting 是否为工作台配方 + * @return 配方信息,如果提取失败返回 null + */ + private static RecipeInfo extractRecipeInfo( + Recipe recipe, + IRecipeLayoutDrawable layout, + boolean isCrafting + ) { + try { + IRecipeSlotsView slotsView = layout.getRecipeSlotsView(); + + // 提取输入槽位 + List inputSlots = slotsView.getSlotViews(RecipeIngredientRole.INPUT); + List> inputs = new ArrayList<>(); + + for (IRecipeSlotView slot : inputSlots) { + List slotItems = slot.getItemStacks() + .map(ItemStack::copy) // 复制以保留数量信息 + .toList(); + inputs.add(slotItems); + } + + // 提取输出槽位 + List outputSlots = slotsView.getSlotViews(RecipeIngredientRole.OUTPUT); + List outputs = new ArrayList<>(); + + for (IRecipeSlotView slot : outputSlots) { + slot.getItemStacks() + .map(ItemStack::copy) // 复制以保留数量信息 + .forEach(outputs::add); + } + + return new RecipeInfo(recipe, isCrafting, inputs, outputs); + + } catch (Exception e) { + LOGGER.warn("[RecipeFinder] Failed to extract recipe info for {}: {}", + recipe.getId(), e.getMessage()); + return null; + } + } + /** * 选择最佳配方(优先选择工作台配方) * - * @param recipes 配方列表 - * @return 最佳配方,如果列表为空返回null + * @param recipes 配方信息列表 + * @return 最佳配方信息,如果列表为空返回null */ - public static Recipe selectBestRecipe(List> recipes) { + public static RecipeInfo selectBestRecipe(List recipes) { if (recipes.isEmpty()) { return null; } - // 优先返回CraftingRecipe - for (Recipe recipe : recipes) { - if (recipe instanceof CraftingRecipe) { - return recipe; + // 优先返回工作台配方 + for (RecipeInfo info : recipes) { + if (info.isCraftingRecipe()) { + return info; } } // 没有工作台配方,返回第一个 return recipes.get(0); } - - /** - * 检查配方输出是否匹配目标物品 - */ - private static boolean matchesOutput(Recipe recipe, ItemStack target, Level level) { - try { - ItemStack result = recipe.getResultItem(level.registryAccess()); - if (result.isEmpty()) { - return false; - } - return ItemStack.isSameItemSameTags(result, target); - } 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 { - return recipe.getIngredients().stream() - .anyMatch(ingredient -> ingredient.test(target)); - } catch (Exception e) { - LOGGER.warn("[RecipeFinder] Exception in matchesInput for recipe {}: {}", recipe.getId(), e.getMessage()); - return false; - } - } } 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..5ae208f --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/RecipeInfo.java @@ -0,0 +1,96 @@ +package com.extendedae_plus.util; + +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Recipe; + +import java.util.List; + +/** + * 配方完整信息 + * + *

包含配方的所有输入材料(带数量)和输出物品

+ */ +public class RecipeInfo { + private final Recipe recipe; + private final boolean isCraftingRecipe; + private final List> inputs; // 每个槽位的所有可能物品(包含数量) + private final List outputs; // 输出物品(包含数量) + + public RecipeInfo( + Recipe recipe, + boolean isCraftingRecipe, + List> inputs, + List outputs + ) { + this.recipe = recipe; + this.isCraftingRecipe = isCraftingRecipe; + this.inputs = inputs; + this.outputs = outputs; + } + + /** + * 获取原始配方对象 + */ + public Recipe getRecipe() { + return recipe; + } + + /** + * 是否为工作台配方 + */ + public boolean isCraftingRecipe() { + return isCraftingRecipe; + } + + /** + * 获取输入材料列表 + * + * @return 每个槽位的所有可能物品列表(包含数量) + */ + public List> getInputs() { + return inputs; + } + + /** + * 获取输出物品列表 + * + * @return 输出物品列表(包含数量) + */ + public List getOutputs() { + return outputs; + } + + /** + * 应用 JEI 书签优先级选择最佳输入材料 + * + * @param bookmarkPriorities 书签优先级映射(物品 -> 优先级,数值越小优先级越高) + * @return 选择的材料列表(每个槽位一个物品) + */ + public List selectBestInputs(java.util.Map bookmarkPriorities) { + java.util.List selected = new java.util.ArrayList<>(); + + for (List slotOptions : inputs) { + if (slotOptions.isEmpty()) { + selected.add(ItemStack.EMPTY); + continue; + } + + // 选择优先级最高的物品(如果都不在书签中,选第一个) + ItemStack best = slotOptions.get(0); + int bestPriority = bookmarkPriorities.getOrDefault(best.getItem(), Integer.MAX_VALUE); + + for (int i = 1; i < slotOptions.size(); i++) { + ItemStack option = slotOptions.get(i); + int priority = bookmarkPriorities.getOrDefault(option.getItem(), Integer.MAX_VALUE); + if (priority < bestPriority) { + bestPriority = priority; + best = option; + } + } + + selected.add(best.copy()); + } + + return selected; + } +}