RecipeFinderUtil改用JEI提供的API

This commit is contained in:
GaLi 2026-02-28 15:34:18 +08:00
parent c517d799a8
commit ce09d26fe1
3 changed files with 276 additions and 144 deletions

View File

@ -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<Recipe<?>> recipes = RecipeFinderUtil.findRecipesByIngredient(
ingredient.get(),
mc.level
List<RecipeInfo> 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<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipe);
List<ItemStack> 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书签优先级选择配方材料
*
* <p>对配方的每个 Ingredient选择 JEI 书签中优先级最高的物品</p>
* <p>如果没有在书签中则使用配方默认的第一个物品</p>
* <p>对配方的每个输入槽位选择 JEI 书签中优先级最高的物品</p>
* <p>如果没有在书签中则使用槽位的第一个物品</p>
*
* @param recipe 配方
* @param recipeInfo 配方信息包含完整的输入输出数量
* @return 选择的材料列表
*/
private static List<ItemStack> selectIngredientsWithJeiPriority(Recipe<?> recipe) {
private static List<ItemStack> selectIngredientsWithJeiPriority(RecipeInfo recipeInfo) {
// 获取JEI书签列表并构建优先级映射
List<? extends ITypedIngredient<?>> bookmarks = JeiRuntimeProxy.getBookmarkList();
Map<AEKey, Integer> priorities = new HashMap<>();
Map<Item, Integer> 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<ItemStack> 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);
}
}

View File

@ -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;
/**
* 配方查找工具类
*
* <p>根据物品查找相关配方优先返回工作台配方CraftingRecipe</p>
* <p>使用 JEI API 根据物品查找相关配方返回包含完整数量信息的 RecipeInfo</p>
*/
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<Recipe<?>> findRecipesByIngredient(ITypedIngredient<?> ingredient, Level level) {
if (ingredient.getType() == VanillaTypes.ITEM_STACK) {
ItemStack stack = (ItemStack) ingredient.getIngredient();
return findRecipesByItem(stack, level);
public static List<RecipeInfo> 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<Recipe<?>> findRecipesByItem(ItemStack item, Level level) {
List<Recipe<?>> 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<RecipeInfo> results = new ArrayList<>();
// 查找工作台配方
try {
IRecipeCategory<CraftingRecipe> craftingCategory = recipeManager.getRecipeCategory(RecipeTypes.CRAFTING);
recipeManager.createRecipeLookup(RecipeTypes.CRAFTING)
.limitFocus(List.of(outputFocus))
.get()
.forEach(recipe -> {
// 创建配方布局以获取完整信息
Optional<IRecipeLayoutDrawable<CraftingRecipe>> 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<Recipe<?>> category = (IRecipeCategory<Recipe<?>>) recipeManager.getRecipeCategory(recipeType);
recipeManager.createRecipeLookup(recipeType)
.limitFocus(List.of(outputFocus))
.get()
.forEach(recipe -> {
if (recipe instanceof Recipe<?> rawRecipe) {
// 创建配方布局以获取完整信息
Optional<IRecipeLayoutDrawable<Recipe<?>>> 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 <T> RecipeInfo extractRecipeInfo(
Recipe<?> recipe,
IRecipeLayoutDrawable<T> layout,
boolean isCrafting
) {
try {
IRecipeSlotsView slotsView = layout.getRecipeSlotsView();
// 提取输入槽位
List<IRecipeSlotView> inputSlots = slotsView.getSlotViews(RecipeIngredientRole.INPUT);
List<List<ItemStack>> inputs = new ArrayList<>();
for (IRecipeSlotView slot : inputSlots) {
List<ItemStack> slotItems = slot.getItemStacks()
.map(ItemStack::copy) // 复制以保留数量信息
.toList();
inputs.add(slotItems);
}
// 提取输出槽位
List<IRecipeSlotView> outputSlots = slotsView.getSlotViews(RecipeIngredientRole.OUTPUT);
List<ItemStack> 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<Recipe<?>> recipes) {
public static RecipeInfo selectBestRecipe(List<RecipeInfo> 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;
}
}
}

View File

@ -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;
/**
* 配方完整信息
*
* <p>包含配方的所有输入材料带数量和输出物品</p>
*/
public class RecipeInfo {
private final Recipe<?> recipe;
private final boolean isCraftingRecipe;
private final List<List<ItemStack>> inputs; // 每个槽位的所有可能物品包含数量
private final List<ItemStack> outputs; // 输出物品包含数量
public RecipeInfo(
Recipe<?> recipe,
boolean isCraftingRecipe,
List<List<ItemStack>> inputs,
List<ItemStack> 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<List<ItemStack>> getInputs() {
return inputs;
}
/**
* 获取输出物品列表
*
* @return 输出物品列表包含数量
*/
public List<ItemStack> getOutputs() {
return outputs;
}
/**
* 应用 JEI 书签优先级选择最佳输入材料
*
* @param bookmarkPriorities 书签优先级映射物品 -> 优先级数值越小优先级越高
* @return 选择的材料列表每个槽位一个物品
*/
public List<ItemStack> selectBestInputs(java.util.Map<net.minecraft.world.item.Item, Integer> bookmarkPriorities) {
java.util.List<ItemStack> selected = new java.util.ArrayList<>();
for (List<ItemStack> 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;
}
}