This commit is contained in:
1820982382
2026-08-01 16:42:53 +08:00
parent e2ab0fb78b
commit 7b341980bd
322 changed files with 15941 additions and 0 deletions
@@ -0,0 +1,20 @@
package thaumicenergistics.integration.jei;
import tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverters;
/**
* AE2-JEI-Integration 兼容注册的隔离类。
* 仅被 {@link ThEJeiPlugin} 通过反射调用({@code Class.forName} + {@code getMethod("register").invoke}),
* 本类(及其引用的 ae2jei 库类)只在库存在时才被类加载器解析。
* 因此只装 JEI 时不会因引用缺失而 NoClassDefFoundError,双装时拖拽转换完整生效。
*/
public final class Ae2JeiCompat {
private Ae2JeiCompat() {
}
/** 注册源质转换器到 AE2-JEI-Integration(支持从 JEI 拖拽源质到 AE2 筛选槽)。 */
public static void register() {
IngredientConverters.register(new EssentiaIngredientConverter());
}
}
@@ -0,0 +1,55 @@
package thaumicenergistics.integration.jei;
import java.util.List;
import java.util.Optional;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.recipe.transfer.IRecipeTransferInfo;
import appeng.menu.SlotSemantics;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
/**
* 为奥术合成终端提供 JEI 配方转移的槽位映射。
* 让 JEI 知道哪些槽是合成格(CRAFTING_GRID)、哪些是玩家背包。
* 实际转移走网络由 ThEUseCraftingRecipeTransfer 处理。
*/
public class ArcaneCraftingTransferInfo
implements IRecipeTransferInfo<ContainerArcaneCraftingTerminal, RecipeHolder<CraftingRecipe>> {
@Override
public Class<? extends ContainerArcaneCraftingTerminal> getContainerClass() {
return ContainerArcaneCraftingTerminal.class;
}
@Override
public Optional<MenuType<ContainerArcaneCraftingTerminal>> getMenuType() {
return Optional.of(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get());
}
@Override
public mezz.jei.api.recipe.RecipeType<RecipeHolder<CraftingRecipe>> getRecipeType() {
return RecipeTypes.CRAFTING;
}
@Override
public boolean canHandle(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
return true;
}
@Override
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
return menu.getSlots(appeng.menu.SlotSemantics.CRAFTING_GRID);
}
@Override
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
return menu.getSlots(appeng.menu.SlotSemantics.PLAYER_INVENTORY);
}
}
@@ -0,0 +1,60 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.transfer.IRecipeTransferInfo;
import appeng.menu.SlotSemantics;
import thaumcraft.integration.jei.JeiArcaneRecipe;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
/**
* JEI 配方转移信息:支持 TC4 奥术配方(JeiArcaneRecipe)。
* 与 ArcaneCraftingTransferInfo(vanilla 合成)配合使用。
*/
public class ArcaneRecipeTransferInfo implements IRecipeTransferInfo<ContainerArcaneCraftingTerminal, JeiArcaneRecipe> {
@Override
public Class<? extends ContainerArcaneCraftingTerminal> getContainerClass() {
return ContainerArcaneCraftingTerminal.class;
}
@Override
public Optional<MenuType<ContainerArcaneCraftingTerminal>> getMenuType() {
return Optional.of(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get());
}
@Override
public RecipeType<JeiArcaneRecipe> getRecipeType() {
return ThaumcraftJeiPlugin.ARCANE;
}
@Override
public boolean canHandle(ContainerArcaneCraftingTerminal menu, JeiArcaneRecipe recipe) {
return true;
}
@Override
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, JeiArcaneRecipe recipe) {
return menu.getSlots(SlotSemantics.CRAFTING_GRID);
}
@Override
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, JeiArcaneRecipe recipe) {
List<Slot> inventorySlots = new ArrayList<>();
var playerInv = menu.getPlayerInventory();
for (Slot slot : menu.slots) {
if (slot != null && slot.container == playerInv) {
inventorySlots.add(slot);
}
}
return inventorySlots;
}
}
@@ -0,0 +1,16 @@
package thaumicenergistics.integration.jei;
import thaumcraft.api.aspects.Aspect;
/**
* JEI 自定义成分:持有 Thaumcraft Aspect 实例。
* 不创建虚拟物品,不污染物品注册表。
* JEI 侧边栏直接显示要素图标,支持搜索和拖拽。
*/
public record AspectIngredient(Aspect aspect) {
@Override
public String toString() {
return "AspectIngredient{" + aspect.getTag() + "}";
}
}
@@ -0,0 +1,73 @@
package thaumicenergistics.integration.jei;
import net.minecraft.resources.ResourceLocation;
import mezz.jei.api.ingredients.IIngredientHelper;
import mezz.jei.api.ingredients.subtypes.UidContext;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.tc.TCReflection;
@SuppressWarnings("removal")
public class AspectIngredientHelper implements IIngredientHelper<AspectIngredient> {
public static final AspectIngredientHelper INSTANCE = new AspectIngredientHelper();
private AspectIngredientHelper() {}
@Override
public AspectIngredientType getIngredientType() {
return AspectIngredientType.INSTANCE;
}
@Override
public String getDisplayName(AspectIngredient ingredient) {
if (ingredient == null || ingredient.aspect() == null) return "Unknown";
return ingredient.aspect().getName();
}
@Override
public Object getUid(AspectIngredient ingredient, UidContext context) {
if (ingredient == null || ingredient.aspect() == null) return "thaumcraft:unknown";
ResourceLocation id = TCReflection.getAspectId(ingredient.aspect());
return id != null ? id.toString() : "thaumcraft:" + ingredient.aspect().getTag();
}
@Override
public String getUniqueId(AspectIngredient ingredient, UidContext context) {
return getUid(ingredient, context).toString();
}
@Override
public ResourceLocation getResourceLocation(AspectIngredient ingredient) {
if (ingredient == null || ingredient.aspect() == null) return ResourceLocation.withDefaultNamespace("air");
ResourceLocation id = TCReflection.getAspectId(ingredient.aspect());
return id != null ? id : ResourceLocation.withDefaultNamespace(ingredient.aspect().getTag());
}
@Override
public AspectIngredient copyIngredient(AspectIngredient ingredient) {
return ingredient;
}
@Override
public boolean isValidIngredient(AspectIngredient ingredient) {
return ingredient != null && ingredient.aspect() != null;
}
@Override
public boolean isIngredientOnServer(AspectIngredient ingredient) {
return true;
}
@Override
public String getErrorInfo(AspectIngredient ingredient) {
if (ingredient == null) return "Null AspectIngredient";
if (ingredient.aspect() == null) return "Null Aspect";
return "Aspect: " + ingredient.aspect().getTag();
}
@Override
public String getDisplayModId(AspectIngredient ingredient) {
return "thaumcraft";
}
}
@@ -0,0 +1,50 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.TooltipFlag;
import mezz.jei.api.ingredients.IIngredientRenderer;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.client.gui.AspectGuiRenderer;
public class AspectIngredientRenderer implements IIngredientRenderer<AspectIngredient> {
public static final AspectIngredientRenderer INSTANCE = new AspectIngredientRenderer();
private static final int SIZE = 16;
private AspectIngredientRenderer() {}
@Override
public void render(GuiGraphics guiGraphics, AspectIngredient ingredient) {
if (ingredient != null && ingredient.aspect() != null) {
AspectGuiRenderer.draw(guiGraphics, ingredient.aspect(), 0, 0, SIZE, 1.0f);
}
}
@Override
public List<Component> getTooltip(AspectIngredient ingredient, TooltipFlag tooltipFlag) {
List<Component> tooltip = new ArrayList<>();
if (ingredient == null || ingredient.aspect() == null) {
return tooltip;
}
Aspect aspect = ingredient.aspect();
tooltip.add(Component.literal(aspect.getName()));
tooltip.add(Component.literal(aspect.getTag()).withStyle(style -> style.withColor(0x888888)));
return tooltip;
}
@Override
public int getWidth() {
return SIZE;
}
@Override
public int getHeight() {
return SIZE;
}
}
@@ -0,0 +1,18 @@
package thaumicenergistics.integration.jei;
import mezz.jei.api.ingredients.IIngredientType;
/**
* JEI 自定义成分类型:AspectIngredient。
*/
public class AspectIngredientType implements IIngredientType<AspectIngredient> {
public static final AspectIngredientType INSTANCE = new AspectIngredientType();
private AspectIngredientType() {}
@Override
public Class<? extends AspectIngredient> getIngredientClass() {
return AspectIngredient.class;
}
}
@@ -0,0 +1,57 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.resources.ResourceLocation;
import mezz.jei.api.gui.handlers.IGhostIngredientHandler;
import mezz.jei.api.ingredients.ITypedIngredient;
import thaumicenergistics.common.container.GuiEssentiaCellWorkbench;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.network.CellWorkbenchC2SPacket;
/**
* 源质元件工作台分区格的 JEI 拖拽支持:从 JEI 拖拽要素到 63 个分区格之一 → 添加该要素分区。
* 纯 JEI API 实现,不依赖 AE2-JEI-Integration(只装 JEI 即可用)。
*/
public class CellWorkbenchGhostIngredientHandler implements IGhostIngredientHandler<GuiEssentiaCellWorkbench> {
@Override
public <I> List<Target<I>> getTargetsTyped(GuiEssentiaCellWorkbench gui, ITypedIngredient<I> ingredient, boolean doStart) {
List<Target<I>> targets = new ArrayList<>();
if (!(ingredient.getIngredient() instanceof AspectIngredient)) {
return targets;
}
// 63 个分区格都是拖放目标
for (int i = 0; i < GuiEssentiaCellWorkbench.NUMBER_OF_WIDGETS; i++) {
int col = i % GuiEssentiaCellWorkbench.WIDGETS_PER_ROW;
int row = i / GuiEssentiaCellWorkbench.WIDGETS_PER_ROW;
int wx = gui.getGuiLeft() + GuiEssentiaCellWorkbench.WIDGET_POS_X + col * GuiEssentiaCellWorkbench.WIDGET_SIZE;
int wy = gui.getGuiTop() + GuiEssentiaCellWorkbench.WIDGET_POS_Y + row * GuiEssentiaCellWorkbench.WIDGET_SIZE;
targets.add(new Target<>() {
@Override
public Rect2i getArea() {
return new Rect2i(wx, wy, GuiEssentiaCellWorkbench.WIDGET_SIZE, GuiEssentiaCellWorkbench.WIDGET_SIZE);
}
@Override
public void accept(I ingredient) {
if (ingredient instanceof AspectIngredient ai && ai.aspect() != null) {
ResourceLocation aspectId = TCReflection.getAspectId(ai.aspect());
if (aspectId != null) {
CellWorkbenchC2SPacket.sendAddAspect(aspectId);
}
}
}
});
}
return targets;
}
@Override
public void onComplete() {
}
}
@@ -0,0 +1,48 @@
package thaumicenergistics.integration.jei;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.Nullable;
import mezz.jei.api.ingredients.IIngredientType;
import appeng.api.stacks.GenericStack;
import appeng.api.stacks.AEKey;
import tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverter;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
* 源质转换器:GenericStack(AEssentiaKey) ↔ AspectIngredient。
* 通过 ServiceLoader 自动注册到 AE2-JEI-Integration。
* 实现从 JEI 拖拽源质到 AE2 筛选槽的功能。
*/
public class EssentiaIngredientConverter implements IngredientConverter<AspectIngredient> {
@Override
public IIngredientType<AspectIngredient> getIngredientType() {
return AspectIngredientType.INSTANCE;
}
@Override
public @Nullable AspectIngredient getIngredientFromStack(GenericStack stack) {
if (stack == null) return null;
AEKey what = stack.what();
if (what instanceof AEssentiaKey essentiaKey) {
ResourceLocation id = essentiaKey.getId();
Aspect aspect = Aspect.getAspect(id.getPath());
if (aspect != null) {
return new AspectIngredient(aspect);
}
}
return null;
}
@Override
public @Nullable GenericStack getStackFromIngredient(AspectIngredient ingredient) {
if (ingredient == null || ingredient.aspect() == null) return null;
var aspectId = TCReflection.getAspectId(ingredient.aspect());
if (aspectId == null) return null;
return new GenericStack(AEssentiaKey.of(aspectId), 1);
}
}
@@ -0,0 +1,53 @@
package thaumicenergistics.integration.jei;
import java.util.List;
import java.util.Optional;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.transfer.IRecipeTransferInfo;
import appeng.menu.SlotSemantics;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
/**
* JEI 配方转移信息:告诉 JEI 哪些槽位是合成网格,哪些是玩家背包。
* 这样 JEI 的 "+" 按钮就能将配方转移到奥术合成台。
*/
public class ThEArcaneCraftingTransferInfo implements IRecipeTransferInfo<ContainerArcaneCraftingTerminal, RecipeHolder<CraftingRecipe>> {
@Override
public Class<? extends ContainerArcaneCraftingTerminal> getContainerClass() {
return ContainerArcaneCraftingTerminal.class;
}
@Override
public Optional<MenuType<ContainerArcaneCraftingTerminal>> getMenuType() {
return Optional.of(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get());
}
@Override
public RecipeType<RecipeHolder<CraftingRecipe>> getRecipeType() {
return RecipeTypes.CRAFTING;
}
@Override
public boolean canHandle(ContainerArcaneCraftingTerminal container, RecipeHolder<CraftingRecipe> recipe) {
return true;
}
@Override
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal container, RecipeHolder<CraftingRecipe> recipe) {
return container.getSlots(SlotSemantics.CRAFTING_GRID);
}
@Override
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal container, RecipeHolder<CraftingRecipe> recipe) {
return container.getSlots(SlotSemantics.PLAYER_INVENTORY);
}
}
@@ -0,0 +1,99 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.world.item.ItemStack;
import mezz.jei.api.gui.handlers.IGhostIngredientHandler;
import mezz.jei.api.ingredients.ITypedIngredient;
import appeng.api.stacks.GenericStack;
import appeng.menu.AEBaseMenu;
import appeng.menu.slot.FakeSlot;
/**
* 处理从 JEI 拖拽到 AE2 Screen 的筛选槽。
* 支持两种拖拽方式:
* 1. 拖拽 WrappedGenericStack ItemStack(AE2 原生支持)
* 2. 拖拽 AspectIngredient(自定义成分类型)
*/
public class ThEGhostIngredientHandler implements IGhostIngredientHandler<Screen> {
@Override
public <I> List<Target<I>> getTargetsTyped(Screen screen, ITypedIngredient<I> ingredient, boolean doStart) {
List<Target<I>> targets = new ArrayList<>();
if (!(screen instanceof appeng.client.gui.AEBaseScreen<?> aeScreen)) {
return targets;
}
if (!(aeScreen.getMenu() instanceof AEBaseMenu menu)) {
return targets;
}
if (!canHandleIngredient(ingredient)) {
return targets;
}
for (var slot : menu.slots) {
if (slot instanceof FakeSlot fakeSlot) {
targets.add(new Target<>() {
@Override
public Rect2i getArea() {
return new Rect2i(
aeScreen.getGuiLeft() + fakeSlot.x,
aeScreen.getGuiTop() + fakeSlot.y,
16, 16
);
}
@Override
public void accept(I ingredient) {
handleIngredientDrop(ingredient, fakeSlot);
}
});
}
}
return targets;
}
private <I> boolean canHandleIngredient(ITypedIngredient<I> ingredient) {
if (ingredient.getIngredient() instanceof AspectIngredient) {
return true;
}
if (ingredient.getIngredient() instanceof ItemStack stack) {
return GenericStack.unwrapItemStack(stack) != null;
}
return false;
}
private <I> void handleIngredientDrop(I ingredient, FakeSlot fakeSlot) {
ItemStack targetStack = ItemStack.EMPTY;
if (ingredient instanceof AspectIngredient ai && ai.aspect() != null) {
// AspectIngredient → WrappedGenericStack ItemStack
var aspectId = thaumicenergistics.common.integration.tc.TCReflection.getAspectId(ai.aspect());
if (aspectId != null) {
var key = thaumicenergistics.common.integration.appeng.AEssentiaKey.of(aspectId);
targetStack = GenericStack.wrapInItemStack(key, 0);
}
} else if (ingredient instanceof ItemStack stack) {
// WrappedGenericStack ItemStack(AE2 原生支持,直接放入)
if (GenericStack.unwrapItemStack(stack) != null) {
targetStack = stack;
}
}
if (!targetStack.isEmpty()) {
fakeSlot.set(targetStack);
}
}
@Override
public void onComplete() {
}
}
@@ -0,0 +1,100 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import mezz.jei.api.IModPlugin;
import mezz.jei.api.JeiPlugin;
import mezz.jei.api.registration.IGuiHandlerRegistration;
import mezz.jei.api.registration.IModIngredientRegistration;
import mezz.jei.api.registration.IRecipeTransferRegistration;
import static mezz.jei.api.constants.RecipeTypes.CRAFTING;
import net.minecraft.resources.ResourceLocation;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.GuiEssentiaCellWorkbench;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
* JEI 插件:将源质注册为自定义 IIngredient 类型显示在 JEI 成分列表中。
* 拖拽功能由 AE2-JEI-Integration 模组的 GhostIngredientHandler + IngredientConverter 处理。
* 不创建虚拟物品,不污染物品注册表。
*/
@JeiPlugin
public class ThEJeiPlugin implements IModPlugin {
private static final Codec<AspectIngredient> CODEC = ResourceLocation.CODEC.flatXmap(
rl -> {
Aspect aspect = Aspect.getAspect(rl.getPath());
if (aspect != null) return DataResult.success(new AspectIngredient(aspect));
return DataResult.error(() -> "Unknown aspect: " + rl);
},
ai -> {
ResourceLocation id = TCReflection.getAspectId(ai.aspect());
ResourceLocation result = id != null ? id : ResourceLocation.withDefaultNamespace(ai.aspect().getTag());
return DataResult.success(result);
}
);
public ThEJeiPlugin() {
// 可选集成:AE2-JEI-Integration 存在时注册源质拖拽转换(拖拽进 AE2 筛选槽);
// 不存在时跳过——只装 JEI 也能正常使用 JEI 显示/配方转移/ghost 拖拽。
try {
Class.forName("tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverters");
Class.forName("thaumicenergistics.integration.jei.Ae2JeiCompat")
.getMethod("register").invoke(null);
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration detected, essentia drag-conversion registered");
} catch (ClassNotFoundException e) {
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration not present, essentia drag-conversion unavailable (optional)");
} catch (ReflectiveOperationException e) {
ThaumicEnergistics.LOG.warn("[JEI] AE2-JEI-Integration registration failed: {}", e.toString());
}
}
@Override
public ResourceLocation getPluginUid() {
return ThaumicEnergistics.id("jei");
}
@Override
public void registerIngredients(IModIngredientRegistration registration) {
List<AspectIngredient> ingredients = new ArrayList<>();
if (Aspect.aspects != null) {
for (Aspect aspect : Aspect.aspects.values()) {
if (aspect != null && aspect.getTag() != null) {
ingredients.add(new AspectIngredient(aspect));
}
}
}
registration.register(
AspectIngredientType.INSTANCE,
ingredients,
AspectIngredientHelper.INSTANCE,
AspectIngredientRenderer.INSTANCE,
CODEC
);
}
@Override
public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) {
ThaumicEnergistics.LOG.info("[JEI] Registering ThEUseCraftingRecipeTransfer for ArcaneCraftingTerminal");
// 注册 IRecipeTransferInfo 让 JEI 显示 "+" 按钮
registration.addRecipeTransferHandler(new ArcaneCraftingTransferInfo());
registration.addRecipeTransferHandler(new ArcaneRecipeTransferInfo());
// 注册 IRecipeTransferHandler 让点击 "+" 时走 ME 网络
registration.addRecipeTransferHandler(new ThEUseCraftingRecipeTransfer(registration.getTransferHelper()), CRAFTING);
registration.addRecipeTransferHandler(new ThEUseArcaneRecipeTransfer(registration.getTransferHelper()), ThaumcraftJeiPlugin.ARCANE);
}
@Override
public void registerGuiHandlers(IGuiHandlerRegistration registration) {
// 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能)
registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler());
}
}
@@ -0,0 +1,97 @@
package thaumicenergistics.integration.jei;
import java.util.Objects;
import java.util.Optional;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.core.NonNullList;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Ingredient;
import mezz.jei.api.gui.builder.ITooltipBuilder;
import mezz.jei.api.gui.ingredient.IRecipeSlotsView;
import mezz.jei.api.recipe.RecipeIngredientRole;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandler;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumcraft.integration.jei.JeiArcaneRecipe;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
/**
* 奥术合成终端专用的 JEI 奥术配方转移处理器。
* 点击 "+" 时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。
*/
public class ThEUseArcaneRecipeTransfer
implements IRecipeTransferHandler<ContainerArcaneCraftingTerminal, JeiArcaneRecipe> {
private final IRecipeTransferHandlerHelper helper;
public ThEUseArcaneRecipeTransfer(IRecipeTransferHandlerHelper helper) {
this.helper = helper;
}
@Override
public Class<? extends ContainerArcaneCraftingTerminal> getContainerClass() {
return ContainerArcaneCraftingTerminal.class;
}
@Override
public Optional<MenuType<ContainerArcaneCraftingTerminal>> getMenuType() {
return Optional.of(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get());
}
@Override
public mezz.jei.api.recipe.RecipeType<JeiArcaneRecipe> getRecipeType() {
return ThaumcraftJeiPlugin.ARCANE;
}
@Override
public IRecipeTransferError transferRecipe(
ContainerArcaneCraftingTerminal menu,
JeiArcaneRecipe recipe,
IRecipeSlotsView display,
net.minecraft.world.entity.player.Player player,
boolean maxTransfer,
boolean doTransfer) {
ThaumicEnergistics.LOG.info("[JEI-Arcane] transferRecipe called, recipeId={}, doTransfer={}",
recipe.id(), doTransfer);
boolean craftMissing = AbstractContainerScreen.hasControlDown();
// 预览:始终 null(绿色按钮),由服务端处理实际缺失
if (!doTransfer) {
return null;
}
// 构建模板物品列表(9 格,对应 3x3 合成格)
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var inputs = recipe.inputs();
for (int i = 0; i < templateItems.size() && i < inputs.size(); i++) {
var variants = inputs.get(i);
if (variants != null && !variants.isEmpty()) {
templateItems.set(i, variants.get(0).copyWithCount(1));
}
}
// 奥术配方不在 vanilla 配方管理器中,recipeId 传 null
// 服务端会用 Ingredient.of(templateItems) 重建,然后从 ME 网络提取
var packet = new FillCraftingGridFromRecipePacket(null, templateItems, craftMissing);
ThaumicEnergistics.LOG.info("[JEI-Arcane] Sending FillCraftingGridFromRecipePacket, templateCount={}",
templateItems.stream().filter(s -> !s.isEmpty()).count());
PacketDistributor.sendToServer(packet);
return null;
}
}
@@ -0,0 +1,236 @@
package thaumicenergistics.integration.jei;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.core.NonNullList;
import net.minecraft.resources.ResourceLocation;
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.RecipeHolder;
import net.minecraft.world.item.crafting.RecipeType;
import net.neoforged.neoforge.network.PacketDistributor;
import mezz.jei.api.gui.builder.ITooltipBuilder;
import mezz.jei.api.gui.ingredient.IRecipeSlotsView;
import mezz.jei.api.recipe.RecipeIngredientRole;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandler;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import appeng.util.CraftingRecipeUtil;
import net.minecraft.world.inventory.MenuType;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
import static appeng.integration.modules.itemlists.TransferHelper.BLUE_PLUS_BUTTON_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.BLUE_SLOT_HIGHLIGHT_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.ORANGE_PLUS_BUTTON_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.RED_SLOT_HIGHLIGHT_COLOR;
/**
* 奥术合成终端专用的 JEI 配方转移处理器。
* 预览时使用 findMissingIngredients 检查 ME 网络物品可用性,
* 实际转移时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。
*
* 参考 AE2-JEI-Integration 的 UseCraftingRecipeTransfer 实现。
*/
public class ThEUseCraftingRecipeTransfer
implements IRecipeTransferHandler<ContainerArcaneCraftingTerminal, RecipeHolder<CraftingRecipe>> {
private static final int CRAFTING_GRID_WIDTH = 3;
private static final int CRAFTING_GRID_HEIGHT = 3;
private final IRecipeTransferHandlerHelper helper;
public ThEUseCraftingRecipeTransfer(IRecipeTransferHandlerHelper helper) {
this.helper = helper;
}
@Override
public Class<? extends ContainerArcaneCraftingTerminal> getContainerClass() {
return ContainerArcaneCraftingTerminal.class;
}
@Override
public Optional<MenuType<ContainerArcaneCraftingTerminal>> getMenuType() {
return Optional.of(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get());
}
@Override
public mezz.jei.api.recipe.RecipeType<RecipeHolder<CraftingRecipe>> getRecipeType() {
return mezz.jei.api.constants.RecipeTypes.CRAFTING;
}
@Override
public IRecipeTransferError transferRecipe(
ContainerArcaneCraftingTerminal menu,
RecipeHolder<CraftingRecipe> recipeHolder,
IRecipeSlotsView display,
net.minecraft.world.entity.player.Player player,
boolean maxTransfer,
boolean doTransfer) {
var recipe = recipeHolder.value();
ThaumicEnergistics.LOG.info("[JEI] transferRecipe called, recipeId={}, doTransfer={}, player={}",
recipeHolder.id(), doTransfer, player.getName().getString());
if (recipe.getType() != RecipeType.CRAFTING) {
ThaumicEnergistics.LOG.warn("[JEI] Not a crafting recipe, type={}", recipe.getType());
return helper.createInternalError();
}
if (recipe.getIngredients().isEmpty()) {
return helper.createInternalError();
}
if (!recipe.canCraftInDimensions(CRAFTING_GRID_WIDTH, CRAFTING_GRID_HEIGHT)) {
return helper.createInternalError();
}
boolean craftMissing = AbstractContainerScreen.hasControlDown();
// 自行构建 slot→ingredient 映射(0-8 对应 3x3 合成格),
// 不依赖 helper.getGuiSlotIndexToIngredientMap(需要 IRecipeTransferInfo 才能正确工作)
var slotToIngredientMap = buildSlotToIngredientMap(recipe);
var missingSlots = menu.findMissingIngredients(slotToIngredientMap);
ThaumicEnergistics.LOG.info("[JEI] missingSlots: missing={}, craftable={}, total={}",
missingSlots.missingSlots().size(), missingSlots.craftableSlots().size(), missingSlots.totalSize());
// 全部缺失 → 红色,无法点击
if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) {
ThaumicEnergistics.LOG.info("[JEI] All items missing, returning NO_ITEMS error");
var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT);
var missingSlotViews = missingSlots.missingSlots().stream()
.map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null)
.filter(Objects::nonNull)
.toList();
return helper.createUserErrorForMissingSlots(
appeng.core.localization.ItemModText.NO_ITEMS.text(), missingSlotViews);
}
// 预览:检查缺失情况,返回对应的错误渲染
if (!doTransfer) {
if (missingSlots.totalSize() != 0) {
int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR;
return new MissingSlotsError(missingSlots, craftMissing, color);
}
// 全部物品都有 → null = 绿色按钮
return null;
}
// 实际转移:发送 ME 网络填充包
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe);
for (int i = 0; i < ingredients.size() && i < 9; i++) {
var ingredient = ingredients.get(i);
if (!ingredient.isEmpty()) {
var items = ingredient.getItems();
if (items.length > 0) {
templateItems.set(i, items[0].copyWithCount(1));
}
}
}
@Nullable
ResourceLocation recipeId = recipeHolder.id();
if (recipeId != null && player.level().getRecipeManager().byKey(recipeId).isEmpty()) {
recipeId = null;
}
var packet = new FillCraftingGridFromRecipePacket(recipeId, templateItems, craftMissing);
ThaumicEnergistics.LOG.info("[JEI] Sending FillCraftingGridFromRecipePacket, recipeId={}, templateCount={}, craftMissing={}",
recipeId, templateItems.stream().filter(s -> !s.isEmpty()).count(), craftMissing);
PacketDistributor.sendToServer(packet);
return null;
}
/**
* 根据配方构建 3x3 合成格的 slot index → ingredient 映射。
* 使用 CraftingRecipeUtil.ensure3by3CraftingMatrix 确保 9 格对齐,
* 与实际转移时的索引体系完全一致。
*/
private static Map<Integer, Ingredient> buildSlotToIngredientMap(CraftingRecipe recipe) {
var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe);
Map<Integer, Ingredient> map = new HashMap<>();
for (int i = 0; i < ingredients.size() && i < 9; i++) {
var ing = ingredients.get(i);
if (!ing.isEmpty()) {
map.put(i, ing);
}
}
return map;
}
/**
* 缺失/可合成物品的预览错误渲染器。
* 参考 AE2-JEI-Integration.UseCraftingRecipeTransfer.ErrorRenderer。
*/
private record MissingSlotsError(
ContainerArcaneCraftingTerminal.MissingIngredientSlots indices,
boolean craftMissing,
int color) implements IRecipeTransferError {
@Override
public Type getType() {
return Type.COSMETIC;
}
@Override
public int getButtonHighlightColor() {
return color;
}
@Override
public void showError(GuiGraphics guiGraphics, int mouseX, int mouseY,
IRecipeSlotsView slots, int recipeX, int recipeY) {
var poseStack = guiGraphics.pose();
poseStack.pushPose();
poseStack.translate(recipeX, recipeY, 0);
var slotViews = slots.getSlotViews(RecipeIngredientRole.INPUT);
for (int i = 0; i < slotViews.size(); i++) {
var slotView = slotViews.get(i);
boolean missing = indices.missingSlots().contains(i);
boolean craftable = indices.craftableSlots().contains(i);
if (missing || craftable) {
slotView.drawHighlight(
guiGraphics,
missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR);
}
}
poseStack.popPose();
}
@Override
public int getMissingCountHint() {
return indices.missingSlots().size();
}
@Override
public void getTooltip(ITooltipBuilder tooltip) {
int missing = indices.missingSlots().size();
int craftable = indices.craftableSlots().size();
if (missing > 0) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.missing", missing));
}
if (craftable > 0) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.craftable", craftable));
}
if (craftMissing) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.ctrl.click"));
}
}
}
}