适配流体配方

This commit is contained in:
GaLi 2026-02-28 16:01:43 +08:00
parent ce09d26fe1
commit 9b8d712c94
5 changed files with 202 additions and 66 deletions

View File

@ -131,6 +131,9 @@ dependencies {
modCompileOnly "curse.maven:ftb-library-forge-404465:6807424" modCompileOnly "curse.maven:ftb-library-forge-404465:6807424"
modRuntimeOnly "curse.maven:ftb-teams-forge-404468:6130786" modRuntimeOnly "curse.maven:ftb-teams-forge-404468:6130786"
modRuntimeOnly "curse.maven:ftb-library-forge-404465:6807424" modRuntimeOnly "curse.maven:ftb-library-forge-404465:6807424"
//MEK
modImplementation "curse.maven:mekanism-268560:6552911"
} }
compileJava { compileJava {

View File

@ -88,12 +88,16 @@ public class CtrlQPatternKeyHandler {
// 应用JEI书签优先级选择材料 // 应用JEI书签优先级选择材料
List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipeInfo); List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(selectedRecipeInfo);
// 获取输出材料转换为 ItemStack流体会被包装
List<ItemStack> selectedOutputs = convertOutputsToItemStacks(selectedRecipeInfo);
// 发送网络包到服务器 // 发送网络包到服务器
ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket( ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket(
selectedRecipeInfo.getRecipe().getId(), selectedRecipeInfo.getRecipe().getId(),
selectedRecipeInfo.isCraftingRecipe(), selectedRecipeInfo.isCraftingRecipe(),
selectedIngredients selectedIngredients,
selectedOutputs
)); ));
// 消耗事件防止传播 // 消耗事件防止传播
@ -125,4 +129,25 @@ public class CtrlQPatternKeyHandler {
// 使用 RecipeInfo 的方法选择最佳输入 // 使用 RecipeInfo 的方法选择最佳输入
return recipeInfo.selectBestInputs(priorities); return recipeInfo.selectBestInputs(priorities);
} }
/**
* 将配方输出转换为 ItemStack 列表用于网络传输
*
* <p>物品直接转换流体会被包装为 GenericStack.wrapInItemStack</p>
*
* @param recipeInfo 配方信息
* @return ItemStack 列表流体已包装
*/
private static List<ItemStack> 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();
}
} }

View File

@ -35,18 +35,20 @@ import net.minecraftforge.network.NetworkEvent;
* C2S: Ctrl+Q快速创建样板数据包 * C2S: Ctrl+Q快速创建样板数据包
* *
* <p> * <p>
* 从客户端发送配方ID和选择的材料到服务器服务器消耗空白样板并创建编码样板掉落到玩家脚下</p> * 从客户端发送配方ID选择的材料和输出到服务器服务器消耗空白样板并创建编码样板掉落到玩家脚下</p>
*/ */
public class CreateCtrlQPatternC2SPacket { public class CreateCtrlQPatternC2SPacket {
private final ResourceLocation recipeId; private final ResourceLocation recipeId;
private final boolean isCraftingPattern; private final boolean isCraftingPattern;
private final List<ItemStack> selectedIngredients; private final List<ItemStack> selectedIngredients;
private final List<ItemStack> outputs; // 输出材料物品或包装的流体
public CreateCtrlQPatternC2SPacket(ResourceLocation recipeId, boolean isCraftingPattern, List<ItemStack> selectedIngredients) { public CreateCtrlQPatternC2SPacket(ResourceLocation recipeId, boolean isCraftingPattern, List<ItemStack> selectedIngredients, List<ItemStack> outputs) {
this.recipeId = recipeId; this.recipeId = recipeId;
this.isCraftingPattern = isCraftingPattern; this.isCraftingPattern = isCraftingPattern;
this.selectedIngredients = selectedIngredients; this.selectedIngredients = selectedIngredients;
this.outputs = outputs;
} }
public static void encode(CreateCtrlQPatternC2SPacket msg, FriendlyByteBuf buf) { public static void encode(CreateCtrlQPatternC2SPacket msg, FriendlyByteBuf buf) {
@ -56,17 +58,26 @@ public class CreateCtrlQPatternC2SPacket {
for (ItemStack stack : msg.selectedIngredients) { for (ItemStack stack : msg.selectedIngredients) {
buf.writeItem(stack); buf.writeItem(stack);
} }
buf.writeInt(msg.outputs.size());
for (ItemStack stack : msg.outputs) {
buf.writeItem(stack);
}
} }
public static CreateCtrlQPatternC2SPacket decode(FriendlyByteBuf buf) { public static CreateCtrlQPatternC2SPacket decode(FriendlyByteBuf buf) {
ResourceLocation recipeId = buf.readResourceLocation(); ResourceLocation recipeId = buf.readResourceLocation();
boolean isCraftingPattern = buf.readBoolean(); boolean isCraftingPattern = buf.readBoolean();
int count = buf.readInt(); int ingredientCount = buf.readInt();
List<ItemStack> ingredients = new ArrayList<>(); List<ItemStack> ingredients = new ArrayList<>();
for (int i = 0; i < count; i++) { for (int i = 0; i < ingredientCount; i++) {
ingredients.add(buf.readItem()); ingredients.add(buf.readItem());
} }
return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients); int outputCount = buf.readInt();
List<ItemStack> 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<NetworkEvent.Context> ctxSupplier) { public static void handle(CreateCtrlQPatternC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
@ -101,7 +112,7 @@ public class CreateCtrlQPatternC2SPacket {
} }
// 3. 创建样板 // 3. 创建样板
ItemStack pattern = createPattern(recipe, msg.isCraftingPattern, msg.selectedIngredients, player); ItemStack pattern = createPattern(recipe, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player);
if (pattern.isEmpty()) { if (pattern.isEmpty()) {
// 创建失败退还空白样板 // 创建失败退还空白样板
@ -247,15 +258,16 @@ public class CreateCtrlQPatternC2SPacket {
} }
/** /**
* 从配方创建样板 * 从配方创建样板支持物品和流体
* *
* @param recipe 配方 * @param recipe 配方
* @param isCrafting 是否为合成样板 * @param isCrafting 是否为合成样板
* @param selectedIngredients 客户端选择的材料应用JEI优先级后 * @param selectedIngredients 客户端选择的材料应用JEI优先级后流体已包装为 GenericStack.wrapInItemStack
* @param selectedOutputs 客户端传递的输出材料物品或包装的流体
* @param player 玩家 * @param player 玩家
* @return 编码的样板物品 * @return 编码的样板物品
*/ */
private static ItemStack createPattern(Recipe<?> recipe, boolean isCrafting, List<ItemStack> selectedIngredients, ServerPlayer player) { private static ItemStack createPattern(Recipe<?> recipe, boolean isCrafting, List<ItemStack> selectedIngredients, List<ItemStack> selectedOutputs, ServerPlayer player) {
try { try {
if (isCrafting && recipe instanceof CraftingRecipe craftingRecipe) { if (isCrafting && recipe instanceof CraftingRecipe craftingRecipe) {
// ===== 合成样板创建路径 ===== // ===== 合成样板创建路径 =====
@ -289,28 +301,45 @@ public class CreateCtrlQPatternC2SPacket {
return encodedPattern; return encodedPattern;
} else { } else {
// ===== 处理样板创建路径 ===== // ===== 处理样板创建路径支持物品和流体=====
List<GenericStack> inputs = new ArrayList<>(); List<GenericStack> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>(); List<GenericStack> outputs = new ArrayList<>();
// 处理输入 - 使用客户端传入的材料选择 // 处理输入 - 使用客户端传入的材料选择支持流体
for (ItemStack item : selectedIngredients) { for (ItemStack item : selectedIngredients) {
if (!item.isEmpty()) { if (!item.isEmpty()) {
inputs.add(new GenericStack( // 尝试解包 GenericStack流体会被包装在特殊的 ItemStack
AEItemKey.of(item), GenericStack genericStack = GenericStack.unwrapItemStack(item);
item.getCount() 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()); for (ItemStack item : selectedOutputs) {
if (!result.isEmpty()) { if (!item.isEmpty()) {
outputs.add(new GenericStack( // 尝试解包 GenericStack流体会被包装在特殊的 ItemStack
AEItemKey.of(result), GenericStack genericStack = GenericStack.unwrapItemStack(item);
result.getCount() if (genericStack != null) {
)); // 这是一个包装的 GenericStack可能是流体
outputs.add(genericStack);
} else {
// 普通物品
AEItemKey itemKey = AEItemKey.of(item);
if (itemKey != null) {
outputs.add(new GenericStack(itemKey, item.getCount()));
}
}
}
} }
// 使用 encodeProcessingPattern 创建处理样板 // 使用 encodeProcessingPattern 创建处理样板

View File

@ -1,8 +1,12 @@
package com.extendedae_plus.util; 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 com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import mezz.jei.api.constants.RecipeTypes; import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.constants.VanillaTypes; import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.forge.ForgeTypes;
import mezz.jei.api.gui.IRecipeLayoutDrawable; import mezz.jei.api.gui.IRecipeLayoutDrawable;
import mezz.jei.api.gui.ingredient.IRecipeSlotView; import mezz.jei.api.gui.ingredient.IRecipeSlotView;
import mezz.jei.api.gui.ingredient.IRecipeSlotsView; 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.ItemStack;
import net.minecraft.world.item.crafting.CraftingRecipe; import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.Recipe; import net.minecraft.world.item.crafting.Recipe;
import net.minecraftforge.fluids.FluidStack;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -33,9 +38,9 @@ public class RecipeFinderUtil {
private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder"); private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder");
/** /**
* 根据JEI物品查找相关配方仅搜索以该物品为输出的配方 * 根据JEI物品或流体查找相关配方仅搜索以该物品/流体为输出的配方
* *
* @param ingredient JEI物品 * @param ingredient JEI物品或流体
* @return 相关配方信息列表包含完整的输入输出数量 * @return 相关配方信息列表包含完整的输入输出数量
*/ */
public static List<RecipeInfo> findRecipesByIngredient(ITypedIngredient<?> ingredient) { public static List<RecipeInfo> findRecipesByIngredient(ITypedIngredient<?> ingredient) {
@ -46,13 +51,6 @@ public class RecipeFinderUtil {
return List.of(); 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(); IJeiHelpers jeiHelpers = jeiRuntime.getJeiHelpers();
IRecipeManager recipeManager = jeiRuntime.getRecipeManager(); IRecipeManager recipeManager = jeiRuntime.getRecipeManager();
IFocusFactory focusFactory = jeiHelpers.getFocusFactory(); IFocusFactory focusFactory = jeiHelpers.getFocusFactory();
@ -133,15 +131,24 @@ public class RecipeFinderUtil {
LOGGER.warn("[RecipeFinder] Error searching other recipe types: {}", e.getMessage()); LOGGER.warn("[RecipeFinder] Error searching other recipe types: {}", e.getMessage());
} }
LOGGER.debug("[RecipeFinder] Found {} recipes for output: {}", // 记录日志
results.size(), String ingredientDesc;
((ItemStack) ingredient.getIngredient()).getDescriptionId()); 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; return results;
} }
/** /**
* 从配方布局中提取完整的配方信息 * 从配方布局中提取完整的配方信息支持物品和流体
* *
* @param recipe 原始配方对象 * @param recipe 原始配方对象
* @param layout JEI 配方布局包含完整的槽位和数量信息 * @param layout JEI 配方布局包含完整的槽位和数量信息
@ -156,25 +163,35 @@ public class RecipeFinderUtil {
try { try {
IRecipeSlotsView slotsView = layout.getRecipeSlotsView(); IRecipeSlotsView slotsView = layout.getRecipeSlotsView();
// 提取输入槽位 // 提取输入槽位支持物品和流体
List<IRecipeSlotView> inputSlots = slotsView.getSlotViews(RecipeIngredientRole.INPUT); List<IRecipeSlotView> inputSlots = slotsView.getSlotViews(RecipeIngredientRole.INPUT);
List<List<ItemStack>> inputs = new ArrayList<>(); List<List<GenericStack>> inputs = new ArrayList<>();
for (IRecipeSlotView slot : inputSlots) { for (IRecipeSlotView slot : inputSlots) {
List<ItemStack> slotItems = slot.getItemStacks() List<GenericStack> slotStacks = new ArrayList<>();
.map(ItemStack::copy) // 复制以保留数量信息
.toList(); // 提取所有 ITypedIngredient
inputs.add(slotItems); for (ITypedIngredient<?> typedIngredient : slot.getAllIngredients().toList()) {
GenericStack genericStack = convertToGenericStack(typedIngredient);
if (genericStack != null) {
slotStacks.add(genericStack);
}
}
inputs.add(slotStacks);
} }
// 提取输出槽位 // 提取输出槽位支持物品和流体
List<IRecipeSlotView> outputSlots = slotsView.getSlotViews(RecipeIngredientRole.OUTPUT); List<IRecipeSlotView> outputSlots = slotsView.getSlotViews(RecipeIngredientRole.OUTPUT);
List<ItemStack> outputs = new ArrayList<>(); List<GenericStack> outputs = new ArrayList<>();
for (IRecipeSlotView slot : outputSlots) { for (IRecipeSlotView slot : outputSlots) {
slot.getItemStacks() for (ITypedIngredient<?> typedIngredient : slot.getAllIngredients().toList()) {
.map(ItemStack::copy) // 复制以保留数量信息 GenericStack genericStack = convertToGenericStack(typedIngredient);
.forEach(outputs::add); if (genericStack != null) {
outputs.add(genericStack);
}
}
} }
return new RecipeInfo(recipe, isCrafting, inputs, outputs); 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;
}
/** /**
* 选择最佳配方优先选择工作台配方 * 选择最佳配方优先选择工作台配方
* *

View File

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