From 9b8d712c947cf3edbc747df39d06a1b9536b4416 Mon Sep 17 00:00:00 2001 From: GaLi <3096147684@qq.com> Date: Sat, 28 Feb 2026 16:01:43 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=82=E9=85=8D=E6=B5=81=E4=BD=93=E9=85=8D?= =?UTF-8?q?=E6=96=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../client/event/CtrlQPatternKeyHandler.java | 27 +++++- .../pattern/CreateCtrlQPatternC2SPacket.java | 73 +++++++++----- .../util/RecipeFinderUtil.java | 96 ++++++++++++++----- .../com/extendedae_plus/util/RecipeInfo.java | 69 +++++++++---- 5 files changed, 202 insertions(+), 66 deletions(-) diff --git a/build.gradle b/build.gradle index 613d927..20f01cf 100644 --- a/build.gradle +++ b/build.gradle @@ -131,6 +131,9 @@ dependencies { modCompileOnly "curse.maven:ftb-library-forge-404465:6807424" modRuntimeOnly "curse.maven:ftb-teams-forge-404468:6130786" modRuntimeOnly "curse.maven:ftb-library-forge-404465:6807424" + + //MEK + modImplementation "curse.maven:mekanism-268560:6552911" } compileJava { 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 6867d86..2a2467f 100644 --- a/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java +++ b/src/main/java/com/extendedae_plus/client/event/CtrlQPatternKeyHandler.java @@ -88,12 +88,16 @@ public class CtrlQPatternKeyHandler { // 应用JEI书签优先级选择材料 List selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipeInfo); + + // 获取输出材料(转换为 ItemStack,流体会被包装) + List selectedOutputs = convertOutputsToItemStacks(selectedRecipeInfo); // 发送网络包到服务器 ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket( selectedRecipeInfo.getRecipe().getId(), selectedRecipeInfo.isCraftingRecipe(), - selectedIngredients + selectedIngredients, + selectedOutputs )); // 消耗事件,防止传播 @@ -125,4 +129,25 @@ public class CtrlQPatternKeyHandler { // 使用 RecipeInfo 的方法选择最佳输入 return recipeInfo.selectBestInputs(priorities); } + + /** + * 将配方输出转换为 ItemStack 列表(用于网络传输) + * + *

物品直接转换,流体会被包装为 GenericStack.wrapInItemStack

+ * + * @param recipeInfo 配方信息 + * @return ItemStack 列表(流体已包装) + */ + private static List convertOutputsToItemStacks(RecipeInfo recipeInfo) { + return recipeInfo.getOutputs().stream() + .map(genericStack -> { + if (genericStack.what() instanceof appeng.api.stacks.AEItemKey itemKey) { + return itemKey.toStack((int) genericStack.amount()); + } else { + // 流体或其他类型,使用包装 + return appeng.api.stacks.GenericStack.wrapInItemStack(genericStack); + } + }) + .toList(); + } } diff --git a/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java b/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java index 2b679c5..a1121b1 100644 --- a/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/pattern/CreateCtrlQPatternC2SPacket.java @@ -35,18 +35,20 @@ import net.minecraftforge.network.NetworkEvent; * C2S: Ctrl+Q快速创建样板数据包 * *

- * 从客户端发送配方ID和选择的材料到服务器,服务器消耗空白样板并创建编码样板掉落到玩家脚下

+ * 从客户端发送配方ID、选择的材料和输出到服务器,服务器消耗空白样板并创建编码样板掉落到玩家脚下

*/ public class CreateCtrlQPatternC2SPacket { private final ResourceLocation recipeId; private final boolean isCraftingPattern; private final List selectedIngredients; + private final List outputs; // 输出材料(物品或包装的流体) - public CreateCtrlQPatternC2SPacket(ResourceLocation recipeId, boolean isCraftingPattern, List selectedIngredients) { + public CreateCtrlQPatternC2SPacket(ResourceLocation recipeId, boolean isCraftingPattern, List selectedIngredients, List outputs) { this.recipeId = recipeId; this.isCraftingPattern = isCraftingPattern; this.selectedIngredients = selectedIngredients; + this.outputs = outputs; } public static void encode(CreateCtrlQPatternC2SPacket msg, FriendlyByteBuf buf) { @@ -56,17 +58,26 @@ public class CreateCtrlQPatternC2SPacket { for (ItemStack stack : msg.selectedIngredients) { buf.writeItem(stack); } + buf.writeInt(msg.outputs.size()); + for (ItemStack stack : msg.outputs) { + buf.writeItem(stack); + } } public static CreateCtrlQPatternC2SPacket decode(FriendlyByteBuf buf) { ResourceLocation recipeId = buf.readResourceLocation(); boolean isCraftingPattern = buf.readBoolean(); - int count = buf.readInt(); + int ingredientCount = buf.readInt(); List ingredients = new ArrayList<>(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < ingredientCount; i++) { ingredients.add(buf.readItem()); } - return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients); + int outputCount = buf.readInt(); + List outputs = new ArrayList<>(); + for (int i = 0; i < outputCount; i++) { + outputs.add(buf.readItem()); + } + return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs); } public static void handle(CreateCtrlQPatternC2SPacket msg, Supplier ctxSupplier) { @@ -101,7 +112,7 @@ public class CreateCtrlQPatternC2SPacket { } // 3. 创建样板 - ItemStack pattern = createPattern(recipe, msg.isCraftingPattern, msg.selectedIngredients, player); + ItemStack pattern = createPattern(recipe, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player); if (pattern.isEmpty()) { // 创建失败,退还空白样板 @@ -247,15 +258,16 @@ public class CreateCtrlQPatternC2SPacket { } /** - * 从配方创建样板 + * 从配方创建样板(支持物品和流体) * * @param recipe 配方 * @param isCrafting 是否为合成样板 - * @param selectedIngredients 客户端选择的材料(应用JEI优先级后) + * @param selectedIngredients 客户端选择的材料(应用JEI优先级后,流体已包装为 GenericStack.wrapInItemStack) + * @param selectedOutputs 客户端传递的输出材料(物品或包装的流体) * @param player 玩家 * @return 编码的样板物品 */ - private static ItemStack createPattern(Recipe recipe, boolean isCrafting, List selectedIngredients, ServerPlayer player) { + private static ItemStack createPattern(Recipe recipe, boolean isCrafting, List selectedIngredients, List selectedOutputs, ServerPlayer player) { try { if (isCrafting && recipe instanceof CraftingRecipe craftingRecipe) { // ===== 合成样板创建路径 ===== @@ -289,28 +301,45 @@ public class CreateCtrlQPatternC2SPacket { 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() - )); + // 尝试解包 GenericStack(流体会被包装在特殊的 ItemStack 中) + GenericStack genericStack = GenericStack.unwrapItemStack(item); + if (genericStack != null) { + // 这是一个包装的 GenericStack(可能是流体) + inputs.add(genericStack); + } else { + // 普通物品 + AEItemKey itemKey = AEItemKey.of(item); + if (itemKey != null) { + inputs.add(new GenericStack(itemKey, item.getCount())); + } + } } } - // 处理输出 - ItemStack result = recipe.getResultItem(player.level().registryAccess()); - if (!result.isEmpty()) { - outputs.add(new GenericStack( - AEItemKey.of(result), - result.getCount() - )); + // 处理输出 - 使用客户端传入的输出(支持流体) + for (ItemStack item : selectedOutputs) { + if (!item.isEmpty()) { + // 尝试解包 GenericStack(流体会被包装在特殊的 ItemStack 中) + GenericStack genericStack = GenericStack.unwrapItemStack(item); + if (genericStack != null) { + // 这是一个包装的 GenericStack(可能是流体) + outputs.add(genericStack); + } else { + // 普通物品 + AEItemKey itemKey = AEItemKey.of(item); + if (itemKey != null) { + outputs.add(new GenericStack(itemKey, item.getCount())); + } + } + } } // 使用 encodeProcessingPattern 创建处理样板 diff --git a/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java index 81cc640..4fbce20 100644 --- a/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java +++ b/src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java @@ -1,8 +1,12 @@ 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.forge.ForgeTypes; import mezz.jei.api.gui.IRecipeLayoutDrawable; import mezz.jei.api.gui.ingredient.IRecipeSlotView; import mezz.jei.api.gui.ingredient.IRecipeSlotsView; @@ -17,6 +21,7 @@ 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.minecraftforge.fluids.FluidStack; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,9 +38,9 @@ public class RecipeFinderUtil { private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder"); /** - * 根据JEI物品查找相关配方(仅搜索以该物品为输出的配方) + * 根据JEI物品或流体查找相关配方(仅搜索以该物品/流体为输出的配方) * - * @param ingredient JEI物品 + * @param ingredient JEI物品或流体 * @return 相关配方信息列表(包含完整的输入输出数量) */ public static List findRecipesByIngredient(ITypedIngredient ingredient) { @@ -46,13 +51,6 @@ public class RecipeFinderUtil { return List.of(); } - // 只支持物品类型 - 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(); - } - IJeiHelpers jeiHelpers = jeiRuntime.getJeiHelpers(); IRecipeManager recipeManager = jeiRuntime.getRecipeManager(); IFocusFactory focusFactory = jeiHelpers.getFocusFactory(); @@ -133,15 +131,24 @@ public class RecipeFinderUtil { LOGGER.warn("[RecipeFinder] Error searching other recipe types: {}", e.getMessage()); } - LOGGER.debug("[RecipeFinder] Found {} recipes for output: {}", - results.size(), - ((ItemStack) ingredient.getIngredient()).getDescriptionId()); + // 记录日志 + String ingredientDesc; + if (ingredient.getType() == VanillaTypes.ITEM_STACK) { + ingredientDesc = ((ItemStack) ingredient.getIngredient()).getDescriptionId(); + } else if (ingredient.getType() == ForgeTypes.FLUID_STACK) { + FluidStack fluidStack = (FluidStack) ingredient.getIngredient(); + ingredientDesc = fluidStack.getFluid().toString(); + } else { + ingredientDesc = ingredient.toString(); + } + + LOGGER.debug("[RecipeFinder] Found {} recipes for output: {}", results.size(), ingredientDesc); return results; } /** - * 从配方布局中提取完整的配方信息 + * 从配方布局中提取完整的配方信息(支持物品和流体) * * @param recipe 原始配方对象 * @param layout JEI 配方布局(包含完整的槽位和数量信息) @@ -156,25 +163,35 @@ public class RecipeFinderUtil { try { IRecipeSlotsView slotsView = layout.getRecipeSlotsView(); - // 提取输入槽位 + // 提取输入槽位(支持物品和流体) List inputSlots = slotsView.getSlotViews(RecipeIngredientRole.INPUT); - List> inputs = new ArrayList<>(); + List> inputs = new ArrayList<>(); for (IRecipeSlotView slot : inputSlots) { - List slotItems = slot.getItemStacks() - .map(ItemStack::copy) // 复制以保留数量信息 - .toList(); - inputs.add(slotItems); + List slotStacks = new ArrayList<>(); + + // 提取所有 ITypedIngredient + for (ITypedIngredient typedIngredient : slot.getAllIngredients().toList()) { + GenericStack genericStack = convertToGenericStack(typedIngredient); + if (genericStack != null) { + slotStacks.add(genericStack); + } + } + + inputs.add(slotStacks); } - // 提取输出槽位 + // 提取输出槽位(支持物品和流体) List outputSlots = slotsView.getSlotViews(RecipeIngredientRole.OUTPUT); - List outputs = new ArrayList<>(); + List outputs = new ArrayList<>(); for (IRecipeSlotView slot : outputSlots) { - slot.getItemStacks() - .map(ItemStack::copy) // 复制以保留数量信息 - .forEach(outputs::add); + for (ITypedIngredient typedIngredient : slot.getAllIngredients().toList()) { + GenericStack genericStack = convertToGenericStack(typedIngredient); + if (genericStack != null) { + outputs.add(genericStack); + } + } } return new RecipeInfo(recipe, isCrafting, inputs, outputs); @@ -186,6 +203,37 @@ public class RecipeFinderUtil { } } + /** + * 将 JEI 的 ITypedIngredient 转换为 AE2 的 GenericStack + * + * @param typedIngredient JEI 类型化材料 + * @return AE2 GenericStack,如果不支持的类型返回 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() == ForgeTypes.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 index 5ae208f..692ec90 100644 --- a/src/main/java/com/extendedae_plus/util/RecipeInfo.java +++ b/src/main/java/com/extendedae_plus/util/RecipeInfo.java @@ -1,26 +1,30 @@ package com.extendedae_plus.util; +import appeng.api.stacks.AEFluidKey; +import appeng.api.stacks.AEItemKey; +import appeng.api.stacks.GenericStack; import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.crafting.Recipe; +import java.util.ArrayList; import java.util.List; /** * 配方完整信息 * - *

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

+ *

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

*/ public class RecipeInfo { private final Recipe recipe; private final boolean isCraftingRecipe; - private final List> inputs; // 每个槽位的所有可能物品(包含数量) - private final List outputs; // 输出物品(包含数量) + private final List> inputs; // 每个槽位的所有可能材料(物品或流体,包含数量) + private final List outputs; // 输出材料(物品或流体,包含数量) public RecipeInfo( Recipe recipe, boolean isCraftingRecipe, - List> inputs, - List outputs + List> inputs, + List outputs ) { this.recipe = recipe; this.isCraftingRecipe = isCraftingRecipe; @@ -45,18 +49,18 @@ public class RecipeInfo { /** * 获取输入材料列表 * - * @return 每个槽位的所有可能物品列表(包含数量) + * @return 每个槽位的所有可能材料列表(物品或流体,包含数量) */ - public List> getInputs() { + public List> getInputs() { return inputs; } /** - * 获取输出物品列表 + * 获取输出材料列表 * - * @return 输出物品列表(包含数量) + * @return 输出材料列表(物品或流体,包含数量) */ - public List getOutputs() { + public List getOutputs() { return outputs; } @@ -64,33 +68,60 @@ public class RecipeInfo { * 应用 JEI 书签优先级选择最佳输入材料 * * @param bookmarkPriorities 书签优先级映射(物品 -> 优先级,数值越小优先级越高) - * @return 选择的材料列表(每个槽位一个物品) + * @return 选择的材料列表(每个槽位一个材料,转换为 ItemStack 用于网络传输) */ public List selectBestInputs(java.util.Map bookmarkPriorities) { - java.util.List selected = new java.util.ArrayList<>(); + List selected = new ArrayList<>(); - for (List slotOptions : inputs) { + 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); + // 选择优先级最高的材料(如果都不在书签中,选第一个) + GenericStack best = slotOptions.get(0); + int bestPriority = getPriority(best, bookmarkPriorities); for (int i = 1; i < slotOptions.size(); i++) { - ItemStack option = slotOptions.get(i); - int priority = bookmarkPriorities.getOrDefault(option.getItem(), Integer.MAX_VALUE); + GenericStack option = slotOptions.get(i); + int priority = getPriority(option, bookmarkPriorities); if (priority < bestPriority) { bestPriority = priority; best = option; } } - selected.add(best.copy()); + // 转换为 ItemStack(流体会被包装) + selected.add(toItemStack(best)); } return selected; } + + /** + * 获取材料的优先级 + */ + private int getPriority(GenericStack stack, java.util.Map priorities) { + if (stack.what() instanceof AEItemKey itemKey) { + return priorities.getOrDefault(itemKey.getItem(), Integer.MAX_VALUE); + } + // 流体没有书签优先级,返回默认值 + return Integer.MAX_VALUE; + } + + /** + * 将 GenericStack 转换为 ItemStack + * + *

物品直接转换,流体会被包装成 GenericStack.wrapInItemStack

+ */ + private ItemStack toItemStack(GenericStack stack) { + if (stack.what() instanceof AEItemKey itemKey) { + return itemKey.toStack((int) stack.amount()); + } else if (stack.what() instanceof AEFluidKey) { + // 流体需要包装成特殊的 ItemStack + return GenericStack.wrapInItemStack(stack); + } + return ItemStack.EMPTY; + } }