From 04ea2681da0e5b5c3232354557dd03aafe88b6fd Mon Sep 17 00:00:00 2001 From: GaLicn <3096147684@qq.com> Date: Thu, 14 Aug 2025 11:46:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0jei=E4=B8=AD=E9=BC=A0?= =?UTF-8?q?=E6=A0=87=E4=B8=AD=E5=BB=BA=E5=8F=AF=E4=BB=A5=E7=9B=B4=E6=8E=A5?= =?UTF-8?q?=E4=B8=8B=E5=8D=95=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/extendedae_plus/ExtendedAEPlus.java | 8 +- .../extendedae_plus/client/InputEvents.java | 48 ++++++++++ .../integration/jei/ExtendedAEJeiPlugin.java | 23 +++++ .../integration/jei/JeiRuntimeProxy.java | 64 ++++++++++++++ .../menu/locator/CuriosItemLocator.java | 55 ++++++++++++ .../extendedae_plus/network/ModNetwork.java | 6 ++ .../network/OpenCraftFromJeiC2SPacket.java | 87 +++++++++++++++++++ .../util/WirelessTerminalLocator.java | 56 ++++++++++-- 8 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/extendedae_plus/client/InputEvents.java create mode 100644 src/main/java/com/extendedae_plus/integration/jei/ExtendedAEJeiPlugin.java create mode 100644 src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java create mode 100644 src/main/java/com/extendedae_plus/menu/locator/CuriosItemLocator.java create mode 100644 src/main/java/com/extendedae_plus/network/OpenCraftFromJeiC2SPacket.java diff --git a/src/main/java/com/extendedae_plus/ExtendedAEPlus.java b/src/main/java/com/extendedae_plus/ExtendedAEPlus.java index 3d22d72..7bb28f9 100644 --- a/src/main/java/com/extendedae_plus/ExtendedAEPlus.java +++ b/src/main/java/com/extendedae_plus/ExtendedAEPlus.java @@ -12,6 +12,8 @@ import com.extendedae_plus.init.ModBlockEntities; import com.extendedae_plus.init.ModItems; import com.extendedae_plus.init.ModCreativeTabs; import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.menu.locator.CuriosItemLocator; +import appeng.menu.locator.MenuLocators; /** * ExtendedAE Plus 主mod类 @@ -45,6 +47,10 @@ public class ExtendedAEPlus { */ private void commonSetup(final FMLCommonSetupEvent event) { // 注册本模组网络通道与数据包 - event.enqueueWork(ModNetwork::register); + event.enqueueWork(() -> { + ModNetwork.register(); + // 注册自定义 Curios 宿主定位器,便于将菜单宿主信息在服务端与客户端间同步 + MenuLocators.register(CuriosItemLocator.class, CuriosItemLocator::writeToPacket, CuriosItemLocator::readFromPacket); + }); } } diff --git a/src/main/java/com/extendedae_plus/client/InputEvents.java b/src/main/java/com/extendedae_plus/client/InputEvents.java new file mode 100644 index 0000000..b089ad6 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/InputEvents.java @@ -0,0 +1,48 @@ +package com.extendedae_plus.client; + +import java.util.Optional; + +import org.lwjgl.glfw.GLFW; + +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.integration.jei.JeiRuntimeProxy; +import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.network.OpenCraftFromJeiC2SPacket; + +import appeng.api.stacks.GenericStack; +import appeng.integration.modules.jei.GenericEntryStackHelper; +import mezz.jei.api.ingredients.ITypedIngredient; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.client.event.ScreenEvent; +import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.common.Mod; + +@Mod.EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE) +public final class InputEvents { + private InputEvents() {} + + @SubscribeEvent + public static void onMouseButtonPre(ScreenEvent.MouseButtonPressed.Pre event) { + // 只处理中键按下 + if (event.getButton() != GLFW.GLFW_MOUSE_BUTTON_MIDDLE) return; + + // 优先在 JEI 配方界面基于坐标获取;若无,再从覆盖层/书签获取 + double mouseX = event.getMouseX(); + double mouseY = event.getMouseY(); + Optional> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY); + if (hovered.isEmpty()) { + hovered = JeiRuntimeProxy.getIngredientUnderMouse(); + } + if (hovered.isEmpty()) return; + + ITypedIngredient typed = hovered.get(); + GenericStack stack = GenericEntryStackHelper.ingredientToStack(typed); + if (stack == null) return; + + // 发送到服务端,让其验证并打开 CraftAmountMenu + ModNetwork.CHANNEL.sendToServer(new OpenCraftFromJeiC2SPacket(stack)); + + // 消费此次点击,避免 JEI/原版对中键的其它处理 + event.setCanceled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/integration/jei/ExtendedAEJeiPlugin.java b/src/main/java/com/extendedae_plus/integration/jei/ExtendedAEJeiPlugin.java new file mode 100644 index 0000000..9320caf --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jei/ExtendedAEJeiPlugin.java @@ -0,0 +1,23 @@ +package com.extendedae_plus.integration.jei; + +import mezz.jei.api.IModPlugin; +import mezz.jei.api.JeiPlugin; +import mezz.jei.api.runtime.IJeiRuntime; +import net.minecraft.resources.ResourceLocation; + +import com.extendedae_plus.ExtendedAEPlus; + +@JeiPlugin +public class ExtendedAEJeiPlugin implements IModPlugin { + private static final ResourceLocation UID = new ResourceLocation(ExtendedAEPlus.MODID, "jei_plugin"); + + @Override + public ResourceLocation getPluginUid() { + return UID; + } + + @Override + public void onRuntimeAvailable(IJeiRuntime jeiRuntime) { + JeiRuntimeProxy.setRuntime(jeiRuntime); + } +} diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java new file mode 100644 index 0000000..5976f6f --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java @@ -0,0 +1,64 @@ +package com.extendedae_plus.integration.jei; + +import java.util.Optional; + +import javax.annotation.Nullable; + +import mezz.jei.api.ingredients.ITypedIngredient; +import mezz.jei.api.constants.VanillaTypes; +import mezz.jei.api.runtime.IBookmarkOverlay; +import mezz.jei.api.runtime.IIngredientListOverlay; +import mezz.jei.api.runtime.IJeiRuntime; + +/** + * 线程安全地缓存并访问 JEI Runtime。 + */ +public final class JeiRuntimeProxy { + private static volatile IJeiRuntime RUNTIME; + + private JeiRuntimeProxy() {} + + static void setRuntime(IJeiRuntime runtime) { + RUNTIME = runtime; + } + + @Nullable + public static IJeiRuntime get() { + return RUNTIME; + } + + public static Optional> getIngredientUnderMouse() { + IJeiRuntime rt = RUNTIME; + if (rt == null) return Optional.empty(); + + IIngredientListOverlay list = rt.getIngredientListOverlay(); + if (list != null) { + var ing = list.getIngredientUnderMouse(); + if (ing.isPresent()) return ing.map(i -> (ITypedIngredient) i); + } + IBookmarkOverlay bm = rt.getBookmarkOverlay(); + if (bm != null) { + var ing = bm.getIngredientUnderMouse(); + if (ing.isPresent()) return ing.map(i -> (ITypedIngredient) i); + } + return Optional.empty(); + } + + /** + * 在 JEI 配方界面区域内,基于屏幕坐标查询鼠标下的配料(优先物品,其次流体)。 + */ + public static Optional> getIngredientUnderMouse(double mouseX, double mouseY) { + IJeiRuntime rt = RUNTIME; + if (rt == null || rt.getRecipesGui() == null) return Optional.empty(); + + var ingredientManager = rt.getIngredientManager(); + + // 支持物品(通用且所有版本可用)。如需流体可后续按版本判断再扩展 + var item = rt.getRecipesGui().getIngredientUnderMouse(VanillaTypes.ITEM_STACK) + .flatMap(v -> ingredientManager.createTypedIngredient(VanillaTypes.ITEM_STACK, v)) + .map(x -> (ITypedIngredient) x); + if (item.isPresent()) return Optional.of(item.get()); + + return Optional.empty(); + } +} diff --git a/src/main/java/com/extendedae_plus/menu/locator/CuriosItemLocator.java b/src/main/java/com/extendedae_plus/menu/locator/CuriosItemLocator.java new file mode 100644 index 0000000..419d8b6 --- /dev/null +++ b/src/main/java/com/extendedae_plus/menu/locator/CuriosItemLocator.java @@ -0,0 +1,55 @@ +package com.extendedae_plus.menu.locator; + +import org.jetbrains.annotations.Nullable; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +import appeng.api.implementations.menuobjects.IMenuItem; +import appeng.api.implementations.menuobjects.ItemMenuHost; +import appeng.menu.locator.MenuLocator; + +// Curios API (软依赖) +import top.theillusivec4.curios.api.CuriosApi; +import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler; + +/** + * 适配 Curios 槽位的自定义 MenuLocator: + * 通过 slotId + index 在两端查找 Curios 实际物品引用,确保 NBT 变化(如耗电)能持久化。 + */ +public record CuriosItemLocator(String slotId, int index) implements MenuLocator { + @Override + @Nullable + public T locate(Player player, Class hostInterface) { + try { + var resolved = CuriosApi.getCuriosInventory(player).resolve(); + if (resolved.isPresent()) { + var handler = resolved.get(); + ICurioStacksHandler stacksHandler = handler.getCurios().get(slotId); + if (stacksHandler != null) { + ItemStack it = stacksHandler.getStacks().getStackInSlot(index); + if (!it.isEmpty() && it.getItem() instanceof IMenuItem guiItem) { + ItemMenuHost menuHost = guiItem.getMenuHost(player, -1, it, null); + if (hostInterface.isInstance(menuHost)) { + return hostInterface.cast(menuHost); + } + } + } + } + } catch (Throwable ignored) { + } + return null; + } + + public void writeToPacket(FriendlyByteBuf buf) { + buf.writeUtf(slotId); + buf.writeVarInt(index); + } + + public static CuriosItemLocator readFromPacket(FriendlyByteBuf buf) { + String slotId = buf.readUtf(); + int index = buf.readVarInt(); + return new CuriosItemLocator(slotId, index); + } +} diff --git a/src/main/java/com/extendedae_plus/network/ModNetwork.java b/src/main/java/com/extendedae_plus/network/ModNetwork.java index 397554b..4be045e 100644 --- a/src/main/java/com/extendedae_plus/network/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/network/ModNetwork.java @@ -24,6 +24,12 @@ public class ModNetwork { .decoder(PickFromWirelessC2SPacket::decode) .consumerNetworkThread(PickFromWirelessC2SPacket::handle) .add(); + + CHANNEL.messageBuilder(OpenCraftFromJeiC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(OpenCraftFromJeiC2SPacket::encode) + .decoder(OpenCraftFromJeiC2SPacket::decode) + .consumerNetworkThread(OpenCraftFromJeiC2SPacket::handle) + .add(); } private static int nextId() { return id++; } diff --git a/src/main/java/com/extendedae_plus/network/OpenCraftFromJeiC2SPacket.java b/src/main/java/com/extendedae_plus/network/OpenCraftFromJeiC2SPacket.java new file mode 100644 index 0000000..93dd70d --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/OpenCraftFromJeiC2SPacket.java @@ -0,0 +1,87 @@ +package com.extendedae_plus.network; + +import java.util.function.Supplier; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; + +import net.minecraftforge.network.NetworkEvent; + +import appeng.api.networking.IGrid; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.items.tools.powered.WirelessTerminalItem; +import appeng.menu.locator.MenuLocators; +import com.extendedae_plus.menu.locator.CuriosItemLocator; +import appeng.menu.me.crafting.CraftAmountMenu; + +import com.extendedae_plus.util.WirelessTerminalLocator; + +/** + * C2S:从 JEI 中键点击请求打开 AE 的下单界面。 + * 负载为一个 GenericStack(物品或流体)。 + */ +public class OpenCraftFromJeiC2SPacket { + private final GenericStack stack; + + public OpenCraftFromJeiC2SPacket(GenericStack stack) { + this.stack = stack; + } + + public static void encode(OpenCraftFromJeiC2SPacket msg, FriendlyByteBuf buf) { + GenericStack.writeBuffer(msg.stack, buf); + } + + public static OpenCraftFromJeiC2SPacket decode(FriendlyByteBuf buf) { + var gs = GenericStack.readBuffer(buf); + return new OpenCraftFromJeiC2SPacket(gs); + } + + public static void handle(OpenCraftFromJeiC2SPacket msg, Supplier ctx) { + var context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null || msg.stack == null) return; + + // 仅支持 AEKey 为可合成的种类 + AEKey what = msg.stack.what(); + + // 定位无线终端 + var located = WirelessTerminalLocator.find(player); + if (located.isEmpty()) return; + + if (!(located.stack.getItem() instanceof WirelessTerminalItem wt)) return; + + // 基本前置校验:联网、电量 + IGrid grid = wt.getLinkedGrid(located.stack, player.level(), player); + if (grid == null) return; + if (!wt.hasPower(player, 0.5, located.stack)) return; + + // 该 Key 是否可被网络自动合成 + var craftingService = grid.getCraftingService(); + if (!craftingService.isCraftable(what)) return; + + var hand = located.getHand(); + int slot = located.getSlotIndex(); + if (hand != null) { + int initial = 1; + CraftAmountMenu.open(player, MenuLocators.forHand(player, hand), what, initial); + } else if (slot >= 0) { + // 直接基于物品槽位作为菜单宿主打开数量输入界面 + int initial = 1; // 初始数量,避免依赖具体 Key 的单位定义 + CraftAmountMenu.open(player, MenuLocators.forInventorySlot(slot), what, initial); + } else { + // Curios 槽位:使用自定义定位器携带 slotId + index 作为菜单宿主 + String curiosSlotId = located.getCuriosSlotId(); + int curiosIndex = located.getCuriosIndex(); + if (curiosSlotId != null && curiosIndex >= 0) { + int initial = 1; + CraftAmountMenu.open(player, new CuriosItemLocator(curiosSlotId, curiosIndex), what, initial); + } else { + // 未知宿主(回退忽略) + } + } + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/util/WirelessTerminalLocator.java b/src/main/java/com/extendedae_plus/util/WirelessTerminalLocator.java index d9bc695..697a8e9 100644 --- a/src/main/java/com/extendedae_plus/util/WirelessTerminalLocator.java +++ b/src/main/java/com/extendedae_plus/util/WirelessTerminalLocator.java @@ -28,46 +28,90 @@ public final class WirelessTerminalLocator { public static final class LocatedTerminal { public final ItemStack stack; private final Consumer setter; + // 在玩家 Inventory 中的槽位索引(0..size-1)。若未知则为 -1。 + private final int slotIndex; + // 若终端在玩家手上,则记录手别;否则为 null。 + private final net.minecraft.world.InteractionHand hand; + // 若终端位于 Curios,则记录其槽位组 ID 与组内索引;否则 slotId 为 null,index 为 -1。 + private final String curiosSlotId; + private final int curiosIndex; public LocatedTerminal(ItemStack stack, Consumer setter) { + this(stack, setter, -1, null, null, -1); + } + + public LocatedTerminal(ItemStack stack, Consumer setter, int slotIndex) { + this(stack, setter, slotIndex, null, null, -1); + } + + public LocatedTerminal(ItemStack stack, Consumer setter, int slotIndex, net.minecraft.world.InteractionHand hand) { + this(stack, setter, slotIndex, hand, null, -1); + } + + public LocatedTerminal(ItemStack stack, Consumer setter, int slotIndex, net.minecraft.world.InteractionHand hand, String curiosSlotId, int curiosIndex) { this.stack = stack; this.setter = setter; + this.slotIndex = slotIndex; + this.hand = hand; + this.curiosSlotId = curiosSlotId; + this.curiosIndex = curiosIndex; } public void set(ItemStack newStack) { this.setter.accept(newStack); } public void commit() { this.setter.accept(this.stack); } public boolean isEmpty() { return this.stack == null || this.stack.isEmpty(); } + /** 若返回 -1,说明不是从原版 Inventory 槽位中找到(比如 Curios)。 */ + public int getSlotIndex() { return this.slotIndex; } + /** 若不为 null,说明终端在玩家手上。 */ + public net.minecraft.world.InteractionHand getHand() { return this.hand; } + /** 若不为 null,说明终端位于 Curios 指定槽位组。 */ + public String getCuriosSlotId() { return this.curiosSlotId; } + /** Curios 组内索引,未知时为 -1。 */ + public int getCuriosIndex() { return this.curiosIndex; } } public static LocatedTerminal find(Player player) { if (player == null) return new LocatedTerminal(ItemStack.EMPTY, s -> {}); - // 1) 原版槽位 + // 1) 先检查主手/副手 + var main = player.getMainHandItem(); + if (!main.isEmpty() && main.getItem() instanceof WirelessTerminalItem) { + return new LocatedTerminal(main, (ns) -> player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, ns), -1, net.minecraft.world.InteractionHand.MAIN_HAND); + } + var off = player.getOffhandItem(); + if (!off.isEmpty() && off.getItem() instanceof WirelessTerminalItem) { + return new LocatedTerminal(off, (ns) -> player.setItemInHand(net.minecraft.world.InteractionHand.OFF_HAND, ns), -1, net.minecraft.world.InteractionHand.OFF_HAND); + } + + // 2) 原版槽位 var inv = player.getInventory(); int size = inv.getContainerSize(); for (int i = 0; i < size; i++) { ItemStack st = inv.getItem(i); if (!st.isEmpty() && st.getItem() instanceof WirelessTerminalItem) { final int slot = i; - return new LocatedTerminal(st, (ns) -> inv.setItem(slot, ns)); + return new LocatedTerminal(st, (ns) -> inv.setItem(slot, ns), slot); } } - // 2) Curios 饰品槽(若已加载) + // 3) Curios 饰品槽(若已加载) if (ModList.get().isLoaded("curios")) { try { // Curios 1.20.x: 通过 CuriosApi.getCuriosInventory 获取 LazyOptional var resolved = CuriosApi.getCuriosInventory(player).resolve(); if (resolved.isPresent()) { ICuriosItemHandler handler = resolved.get(); - for (ICurioStacksHandler stacksHandler : handler.getCurios().values()) { + for (var entry : handler.getCurios().entrySet()) { + String slotId = entry.getKey(); + ICurioStacksHandler stacksHandler = entry.getValue(); IDynamicStackHandler stacks = stacksHandler.getStacks(); int slots = stacks.getSlots(); for (int i = 0; i < slots; i++) { ItemStack st = stacks.getStackInSlot(i); if (!st.isEmpty() && st.getItem() instanceof WirelessTerminalItem) { final int slot = i; - return new LocatedTerminal(st, (ns) -> stacks.setStackInSlot(slot, ns)); + // 记录 Curios 槽位标识与索引,供应后续使用自定义 MenuLocator 打开菜单 + return new LocatedTerminal(st, (ns) -> stacks.setStackInSlot(slot, ns), -1, null, slotId, slot); } } } @@ -77,6 +121,6 @@ public final class WirelessTerminalLocator { } } - return new LocatedTerminal(ItemStack.EMPTY, s -> {}); + return new LocatedTerminal(ItemStack.EMPTY, s -> {}, -1, null); } }