增加jei中鼠标中建可以直接下单功能

This commit is contained in:
GaLicn 2025-08-14 11:46:24 +08:00
parent b5cb61a160
commit 04ea2681da
8 changed files with 340 additions and 7 deletions

View File

@ -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);
});
}
}

View File

@ -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<ITypedIngredient<?>> 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);
}
}

View File

@ -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);
}
}

View File

@ -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<ITypedIngredient<?>> 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<ITypedIngredient<?>> 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();
}
}

View File

@ -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> T locate(Player player, Class<T> 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);
}
}

View File

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

View File

@ -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<NetworkEvent.Context> 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);
}
}

View File

@ -28,46 +28,90 @@ public final class WirelessTerminalLocator {
public static final class LocatedTerminal {
public final ItemStack stack;
private final Consumer<ItemStack> setter;
// 在玩家 Inventory 中的槽位索引0..size-1若未知则为 -1
private final int slotIndex;
// 若终端在玩家手上则记录手别否则为 null
private final net.minecraft.world.InteractionHand hand;
// 若终端位于 Curios则记录其槽位组 ID 与组内索引否则 slotId nullindex -1
private final String curiosSlotId;
private final int curiosIndex;
public LocatedTerminal(ItemStack stack, Consumer<ItemStack> setter) {
this(stack, setter, -1, null, null, -1);
}
public LocatedTerminal(ItemStack stack, Consumer<ItemStack> setter, int slotIndex) {
this(stack, setter, slotIndex, null, null, -1);
}
public LocatedTerminal(ItemStack stack, Consumer<ItemStack> setter, int slotIndex, net.minecraft.world.InteractionHand hand) {
this(stack, setter, slotIndex, hand, null, -1);
}
public LocatedTerminal(ItemStack stack, Consumer<ItemStack> 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);
}
}