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;
+ }
}