diff --git a/build.gradle b/build.gradle index a3e41cd..007bfa3 100644 --- a/build.gradle +++ b/build.gradle @@ -102,6 +102,7 @@ dependencies { //jec modCompileOnly "curse.maven:just-enough-characters-250702:6680042" + //mae2 // modRuntimeOnly "curse.maven:modern-ae2-additions-1028068:6342203" modCompileOnly "curse.maven:modern-ae2-additions-1028068:6342203" diff --git a/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java b/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java index 3e98d51..850836a 100644 --- a/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java +++ b/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.function.Function; /** - * 参照 MAE2 的 DynamicCraftingCubeModelProvider,实现形成态光照模型。 + * 形成态光照模型。 */ public class EPlusCraftingCubeModelProvider extends AbstractCraftingUnitModelProvider { @@ -29,13 +29,13 @@ public class EPlusCraftingCubeModelProvider public static final ChunkRenderTypeSet CUTOUT = ChunkRenderTypeSet.of(RenderType.cutout()); private static final List MATERIALS = new ArrayList<>(); - // 与 MAE2 一致:将环形边框与基础发光底图放在本模组命名空间 + //将环形边框与基础发光底图放在本模组命名空间 protected static final Material RING_CORNER = texture(ExtendedAEPlus.MODID, "ring_corner"); protected static final Material RING_SIDE_HOR = texture(ExtendedAEPlus.MODID, "ring_side_hor"); protected static final Material RING_SIDE_VER = texture(ExtendedAEPlus.MODID, "ring_side_ver"); protected static final Material LIGHT_BASE = texture(ExtendedAEPlus.MODID, "light_base"); - // 我们自己的亮面贴图(formed 时使用) + // 亮面贴图(formed 时使用) protected static final Material ACCELERATOR_4X_LIGHT = texture(ExtendedAEPlus.MODID, "4x_accelerator_light"); protected static final Material ACCELERATOR_16X_LIGHT = texture(ExtendedAEPlus.MODID, diff --git a/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java b/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java deleted file mode 100644 index c6edc7a..0000000 --- a/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.extendedae_plus.mixin.accessor; - -import net.minecraft.client.gui.components.Renderable; -import net.minecraft.client.gui.components.events.GuiEventListener; -import net.minecraft.client.gui.narration.NarratableEntry; -import net.minecraft.client.gui.screens.Screen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(Screen.class) -public interface ScreenInvoker { - @Invoker("addRenderableWidget") - W eap$invokeAddRenderableWidget(W widget); -} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java index 17d1ce8..4194d24 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java @@ -2,21 +2,29 @@ package com.extendedae_plus.mixin.ae2; import appeng.client.Point; import appeng.client.gui.AEBaseScreen; +import appeng.client.gui.StackWithBounds; +import appeng.client.gui.me.crafting.CraftingCPUScreen; import appeng.client.gui.TextOverride; import appeng.client.gui.style.PaletteColor; import appeng.client.gui.style.ScreenStyle; import appeng.client.gui.style.Text; import appeng.client.gui.style.TextAlignment; +import appeng.api.stacks.AEKey; import appeng.menu.slot.AppEngSlot; import com.extendedae_plus.api.ExPatternPageAccessor; +import com.extendedae_plus.network.CraftingMonitorJumpC2SPacket; +import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.network.CraftingMonitorOpenProviderC2SPacket; import com.extendedae_plus.util.GuiUtil; import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider; +import com.mojang.logging.LogUtils; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.renderer.Rect2i; import net.minecraft.network.chat.Component; import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.world.inventory.Slot; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @@ -38,6 +46,74 @@ public abstract class AEBaseScreenMixin { return null; } + /** + * 在 AEBaseScreen 的 mouseClicked 入口拦截 CraftingCPUScreen 的 Shift+左键, + * 读取鼠标下的 AEKey 并发送 CraftingMonitorJumpC2SPacket。 + */ + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + private void eap$craftingCpuShiftLeftClick(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + // 仅处理 CraftingCPUScreen 实例 + Object self = this; + if (!(self instanceof CraftingCPUScreen screen)) { + return; + } + // 仅在 Shift + 左键 时触发 + if (button != 0 || !net.minecraft.client.gui.screens.Screen.hasShiftDown()) { + return; + } + try { + StackWithBounds hovered = screen.getStackUnderMouse(mouseX, mouseY); + if (hovered == null || hovered.stack() == null) { + return; + } + AEKey key = hovered.stack().what(); + if (key == null) { + return; + } + // Debug: 标记一次发送 + try { + LogUtils.getLogger().info("EAP: Send CraftingMonitorJumpC2SPacket: {}", key); + } catch (Throwable ignored2) {} + ModNetwork.CHANNEL.sendToServer(new CraftingMonitorJumpC2SPacket(key)); + cir.setReturnValue(true); + } catch (Throwable ignored) { + } + } + + /** + * 在 AEBaseScreen 的 mouseClicked 入口拦截 CraftingCPUScreen 的 Shift+右键, + * 读取鼠标下的 AEKey 并发送 CraftingMonitorOpenProviderC2SPacket(打开样板供应器UI)。 + */ + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + private void eap$craftingCpuShiftRightClick(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { + // 仅处理 CraftingCPUScreen 实例 + Object self = this; + if (!(self instanceof CraftingCPUScreen screen)) { + return; + } + // 仅在 Shift + 右键 时触发 + if (button != 1 || !net.minecraft.client.gui.screens.Screen.hasShiftDown()) { + return; + } + try { + StackWithBounds hovered = screen.getStackUnderMouse(mouseX, mouseY); + if (hovered == null || hovered.stack() == null) { + return; + } + AEKey key = hovered.stack().what(); + if (key == null) { + return; + } + // Debug: 标记一次发送(打开供应器UI) + try { + LogUtils.getLogger().info("EAP: Send CraftingMonitorOpenProviderC2SPacket: {}", key); + } catch (Throwable ignored2) {} + ModNetwork.CHANNEL.sendToServer(new CraftingMonitorOpenProviderC2SPacket(key)); + cir.setReturnValue(true); + } catch (Throwable ignored) { + } + } + @Unique private static int eap$getIntField(Object self, String name, int def) { Class c = self.getClass(); diff --git a/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java b/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java index b408a42..fde3756 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java @@ -8,6 +8,21 @@ import com.glodblock.github.glodium.network.packet.sync.IActionHolder; import com.glodblock.github.glodium.network.packet.sync.Paras; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.core.registries.Registries; +import net.minecraftforge.network.NetworkHooks; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @@ -17,11 +32,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Map; import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; @Mixin(ContainerExPatternTerminal.class) public abstract class ContainerExPatternTerminalMixin implements IActionHolder { - @GuiSync(11452) + @GuiSync(25564) @Unique public boolean eap$hidePatternSlots = false; @@ -41,13 +58,19 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder { } @Unique - private final Map> eap$actions = createHolder(); + private Map> eap$actions; @Unique private Player epp$player; + @Unique + private static final Logger EAP_LOGGER = LogManager.getLogger("ExtendedAE_Plus"); + @Inject(method = "*", at = @At("TAIL")) private void init(int id, net.minecraft.world.entity.player.Inventory playerInventory, IConfigurableObject host, CallbackInfo ci) { + if (this.eap$actions == null) { + this.eap$actions = createHolder(); + } this.epp$player = playerInventory.player; // 注册上传动作:参数顺序必须与客户端 CGenericPacket 保持一致 this.eap$actions.put("upload", p -> { @@ -61,6 +84,110 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder { } catch (Throwable ignored) { } }); + + // 注册打开UI动作:open_ui(posLong, dimensionId, faceOrdinal?) + this.eap$actions.put("open_ui", p -> { + try { + // 参数解析 + Object po = p.get(0); // BlockPos as long (BlockPos#asLong) + Object do0 = p.get(1); // Dimension id string (e.g., minecraft:overworld) + Object fo; + try { + fo = p.get(2); // Optional face ordinal + } catch (Throwable __ignored) { + fo = null; + } + + long posLong = (po instanceof Number) ? ((Number) po).longValue() : Long.parseLong(String.valueOf(po)); + String dimStr = String.valueOf(do0); + int faceOrd = -1; + if (fo != null) { + faceOrd = (fo instanceof Number) ? ((Number) fo).intValue() : Integer.parseInt(String.valueOf(fo)); + } + + BlockPos pos = BlockPos.of(posLong); + ResourceLocation dimId = ResourceLocation.tryParse(dimStr); + if (dimId == null) { + EAP_LOGGER.warn("[EPlus] open_ui: invalid dim '{}'", dimStr); + return; + } + ResourceKey dimKey = ResourceKey.create(Registries.DIMENSION, dimId); + + if (!(this.epp$player instanceof ServerPlayer sp)) { + EAP_LOGGER.warn("[EPlus] open_ui: not a ServerPlayer"); + return; + } + + ServerLevel level = sp.server.getLevel(dimKey); + if (level == null) { + EAP_LOGGER.warn("[EPlus] open_ui: level null for key {}", dimKey); + return; + } + + EAP_LOGGER.debug("[EPlus] open_ui: pos={}, dim={}, faceOrd={}", pos, dimKey.location(), faceOrd); + + // 目标应为供应器所面向/连接的相邻方块,而非供应器自身 + Direction[] tries = (faceOrd >= 0 && faceOrd < Direction.values().length) + ? new Direction[]{Direction.values()[faceOrd]} + : Direction.values(); + + // 1) 先尝试在相邻方块直接打开 MenuProvider + for (Direction dir : tries) { + BlockPos targetPos = pos.relative(dir); + BlockEntity be = level.getBlockEntity(targetPos); + if (be instanceof MenuProvider provider) { + NetworkHooks.openScreen(sp, provider, targetPos); + EAP_LOGGER.debug("[EPlus] open_ui: opened BE MenuProvider at {} (neighbor via {})", targetPos, dir); + return; + } + var state = level.getBlockState(targetPos); + MenuProvider provider = state.getMenuProvider(level, targetPos); + if (provider != null) { + NetworkHooks.openScreen(sp, provider, targetPos); + EAP_LOGGER.debug("[EPlus] open_ui: opened State MenuProvider at {} (neighbor via {})", targetPos, dir); + return; + } + } + + // 2) 兜底:为避免误触发放置/覆盖,仅在手上至少有一只手为空时,使用 BlockState.use 进行一次“徒手交互” + boolean hasFace = (faceOrd >= 0 && faceOrd < Direction.values().length); + boolean anyHandEmpty = sp.getMainHandItem().isEmpty() || sp.getOffhandItem().isEmpty(); + if (anyHandEmpty) { + InteractionHand hand = sp.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND; + if (hasFace) { + Direction dir = Direction.values()[faceOrd]; + BlockPos targetPos = pos.relative(dir); + var state2 = level.getBlockState(targetPos); + var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), dir.getOpposite(), targetPos, false); + InteractionResult r = state2.use(level, sp, hand, hit); + EAP_LOGGER.debug("[EPlus] open_ui: fallback(state.use) at {} hit {} (via {}), result={}", targetPos, dir.getOpposite(), dir, r); + } else { + // 无朝向:优先尝试有方块实体的邻居,否则尝试实心方块邻居,各只尝试一次 + Direction chosen = null; + for (Direction d : Direction.values()) { + if (level.getBlockEntity(pos.relative(d)) != null) { chosen = d; break; } + } + if (chosen == null) { + for (Direction d : Direction.values()) { + if (!level.getBlockState(pos.relative(d)).isAir()) { chosen = d; break; } + } + } + if (chosen != null) { + BlockPos targetPos = pos.relative(chosen); + var state2 = level.getBlockState(targetPos); + var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false); + InteractionResult r = state2.use(level, sp, hand, hit); + EAP_LOGGER.debug("[EPlus] open_ui: fallback(state.use) at {} hit {} (auto via {}), result={}", targetPos, chosen.getOpposite(), chosen, r); + } else { + EAP_LOGGER.debug("[EPlus] open_ui: no neighbor candidate for fallback (faceOrd<0)"); + } + } + } else { + EAP_LOGGER.debug("[EPlus] open_ui: skip fallback (hands occupied)"); + } + } catch (Throwable ignored) { + } + }); } @NotNull diff --git a/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java b/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java index e529c5c..94655ab 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java @@ -10,13 +10,22 @@ import appeng.client.gui.widgets.AETextField; import appeng.client.gui.widgets.IconButton; import appeng.menu.AEBaseMenu; import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal; +import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.network.OpenProviderUiC2SPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Rect2i; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.Shadow; @@ -29,9 +38,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Set; +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; @Pseudo -@Mixin(GuiExPatternTerminal.class) +@Mixin(value = GuiExPatternTerminal.class) public abstract class GuiExPatternTerminalMixin extends AEBaseScreen { @Unique @@ -46,6 +59,14 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen private boolean eap$showSlots = false; // 默认显示槽位 @Unique private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID + @Unique + private final Map eap$openUIButtons = new HashMap<>(); + + @Unique + private static final Logger EAP_LOGGER = LogManager.getLogger("ExtendedAE_Plus"); + + @Unique + private boolean eap$debugLoggedOnce = false; @Shadow(remap = false) private AETextField searchOutField; @Shadow(remap = false) private AETextField searchInField; @Shadow(remap = false) private Set matchedStack; @@ -109,8 +130,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen /** * 拦截鼠标点击事件,实现Shift+左键快速上传样板功能 + * 注意:某些整合包的 ExtendedAE 版本不在该类中覆写 mouseClicked,此处设置 require=0 以防止注入失败导致崩溃。 */ - @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, require = 0) private void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { // 检查是否是左键点击 + Shift键 if (button == 0 && hasShiftDown()) { @@ -181,6 +203,86 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen } } + @Unique + private int getIntConst(Class cls, String name, int defVal) { + try { + var f = cls.getDeclaredField(name); + f.setAccessible(true); + return (int) f.get(null); + } catch (Throwable t) { + return defVal; + } + } + + @Unique + private void eap$tryOpenProviderUI(int rowIndex) { + try { + // 使用 Accessor 获取 rows,避免取到父类导致失败 + com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc = + (com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this; + java.util.ArrayList rows = acc.getRows(); + + // 找到该分组对应的第一个 PatternContainerRecord + Class cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class; + var byGroupField = cls.getDeclaredField("byGroup"); + byGroupField.setAccessible(true); + Object byGroup = byGroupField.get(this); // HashMultimap + + Object headerRow = rows.get(rowIndex); + var groupField = headerRow.getClass().getDeclaredField("group"); + groupField.setAccessible(true); + Object group = groupField.get(headerRow); + + // 调用 byGroup.get(group),再取第一个元素 + java.util.Collection containers = (java.util.Collection) byGroup.getClass().getMethod("get", Object.class).invoke(byGroup, group); + if (containers == null || containers.isEmpty()) { + return; + } + Object firstRecord = containers.iterator().next(); // PatternContainerRecord + long serverId = (long) firstRecord.getClass().getMethod("getServerId").invoke(firstRecord); + + // 通过 infoMap 获取位置信息 + var infoMapField = cls.getDeclaredField("infoMap"); + infoMapField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.HashMap infoMap = (java.util.HashMap) infoMapField.get(this); + Object info = infoMap.get(serverId); + if (info == null) { + // 无位置信息,提示 + if (this.minecraft != null && this.minecraft.player != null) { + this.minecraft.player.displayClientMessage(Component.literal("未找到该供应器的位置信息,无法打开UI"), true); + } + return; + } + + // PatternProviderInfo record: pos(), face(), playerWorld() + Object pos = info.getClass().getMethod("pos").invoke(info); + Object face = info.getClass().getMethod("face").invoke(info); // 可能为 null(方块型供应器) + Object playerWorld = info.getClass().getMethod("playerWorld").invoke(info); + + // 避免对 MC 类进行反射,使用强制类型转换后直接调用方法(由 Forge 运行时重映射保证) + long posLong = ((BlockPos) pos).asLong(); + String dimStr = ((ResourceKey) playerWorld).location().toString(); + int faceOrd = -1; + if (face != null) { + faceOrd = ((Direction) face).ordinal(); + } + + // 发送我们自己的 C2S 包:OpenProviderUiC2SPacket + try { + ModNetwork.CHANNEL.sendToServer(new OpenProviderUiC2SPacket( + posLong, + new ResourceLocation(dimStr), + faceOrd + )); + } catch (Throwable t) { + // 静默失败:不提示玩家 + } + } catch (Throwable t) { + // 静默失败:不输出日志 + } + } + /** * 重置当前选择的样板供应器ID */ @@ -228,6 +330,70 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen this.addToLeftToolbar(this.eap$toggleSlotsButton); } + /** + * 处理屏幕缩放(resize)后按钮位置未更新的问题: + * - 清理并移除现有的“打开UI”按钮 + * - 尝试重置滚动条并刷新列表 + * 缩放后的下一帧,drawFG 会基于新的 leftPos/topPos 重建与定位按钮 + */ + @Inject(method = "resize", at = @At("TAIL"), remap = false, require = 0) + private void eap$onResize(Minecraft mc, int width, int height, CallbackInfo ci) { + try { + // 移除并清理按钮,避免旧位置残留 + this.eap$openUIButtons.values().forEach(this::removeWidget); + this.eap$openUIButtons.clear(); + + // 重置一次滚动条,避免可见行/偏移在缩放后与 UI 尺寸不一致 + try { + Method resetScrollbarMethod = null; + try { + resetScrollbarMethod = this.getClass().getDeclaredMethod("resetScrollbar"); + } catch (NoSuchMethodException e1) { + try { + resetScrollbarMethod = this.getClass().getSuperclass().getDeclaredMethod("resetScrollbar"); + } catch (NoSuchMethodException e2) { + resetScrollbarMethod = null; + } + } + if (resetScrollbarMethod != null) { + resetScrollbarMethod.setAccessible(true); + resetScrollbarMethod.invoke(this); + } + } catch (Throwable ignored) { + } + + // 刷新列表,使 rows/visibleRows 立即以新尺寸重算 + try { + Method refreshMethod = null; + try { + refreshMethod = this.getClass().getDeclaredMethod("refreshList"); + } catch (NoSuchMethodException e1) { + try { + refreshMethod = this.getClass().getSuperclass().getDeclaredMethod("refreshList"); + } catch (NoSuchMethodException e2) { + refreshMethod = null; + } + } + if (refreshMethod != null) { + refreshMethod.setAccessible(true); + refreshMethod.invoke(this); + } + } catch (Throwable ignored) { + } + + // 下次绘制重新输出一次调试行,便于确认缩放后的 rows/scroll + this.eap$debugLoggedOnce = false; + } catch (Throwable ignored) { + } + } + + @Inject(method = "init", at = @At("TAIL"), remap = false, require = 0) + private void eap$onInit(CallbackInfo ci) { + // 清理旧的打开UI按钮 + this.eap$openUIButtons.values().forEach(this::removeWidget); + this.eap$openUIButtons.clear(); + } + @Inject(method = "refreshList", at = @At("HEAD"), remap = false) private void onRefreshListStart(CallbackInfo ci) { // 更新按钮图标 @@ -236,6 +402,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen this.eap$showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots" ))); } + // 清理旧的打开UI按钮 + this.eap$openUIButtons.values().forEach(this::removeWidget); + this.eap$openUIButtons.clear(); } @Inject(method = "refreshList", at = @At("TAIL"), remap = false) @@ -342,6 +511,63 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen @Inject(method = "drawFG", at = @At("TAIL"), remap = false) private void eap$afterDrawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) { + // 动态放置/创建每个组标题后的“打开UI”按钮 + try { + // 使用 Accessor 获取必要的字段,避免反射失败 + com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc = + (com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this; + java.util.ArrayList rows = acc.getRows(); + int currentScroll = acc.getScrollbar().getCurrentScroll(); + + // 直接引用目标类以获取其静态常量 + Class cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class; + int GUI_PADDING_X = getIntConst(cls, "GUI_PADDING_X", 22); + int GUI_PADDING_Y = getIntConst(cls, "GUI_PADDING_Y", 6); + int GUI_HEADER_HEIGHT = getIntConst(cls, "GUI_HEADER_HEIGHT", 51); + int ROW_HEIGHT = getIntConst(cls, "ROW_HEIGHT", 18); + int TEXT_MAX_WIDTH = getIntConst(cls, "TEXT_MAX_WIDTH", 155); + + int visibleRows = acc.getVisibleRows(); + + // 生产环境移除调试日志 + + // 先隐藏旧按钮,避免残留 + for (Button b : this.eap$openUIButtons.values()) { + b.visible = false; + } + + int shownCount = 0; + for (int i = 0; i < visibleRows; i++) { + int rowIndex = currentScroll + i; + if (rowIndex < 0 || rowIndex >= rows.size()) { + continue; + } + Object row = rows.get(rowIndex); + if (!row.getClass().getSimpleName().equals("GroupHeaderRow")) { + continue; + } + + // 放置按钮:位于名称文本右侧,与原类 choiceButton 锚点相邻,向右偏移 20px + int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 40; + int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 3; + + Button btn = eap$openUIButtons.get(rowIndex); + if (btn == null) { + btn = Button.builder(Component.literal("UI"), (b) -> { + eap$tryOpenProviderUI(rowIndex); + }).size(14, 12).build(); + btn.setTooltip(Tooltip.create(Component.literal("打开该供应器目标容器的界面"))); + eap$openUIButtons.put(rowIndex, btn); + this.addRenderableWidget(btn); + } + btn.setPosition(bx, by); + btn.visible = true; + shownCount++; + } + // 生产环境移除调试日志 + } catch (Throwable ignored) { + } + // 原有的搜索高亮逻辑 // 仅当任一搜索框非空时绘制叠加层(与原版行为保持一致) boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty()) diff --git a/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java b/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java new file mode 100644 index 0000000..eacd863 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java @@ -0,0 +1,155 @@ +package com.extendedae_plus.network; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.networking.crafting.ICraftingProvider; +import appeng.api.networking.security.IActionHost; +import appeng.api.stacks.AEKey; +import appeng.helpers.patternprovider.PatternProviderLogic; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import appeng.me.service.CraftingService; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; +import com.mojang.logging.LogUtils; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraftforge.network.NetworkEvent; +import net.minecraftforge.network.NetworkHooks; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * 客户端从 CraftingCPUScreen 发送:鼠标下条目对应的 AEKey。 + * 服务端在当前打开的 CraftingCPUMenu 所属网络中,定位匹配该 AEKey 的样板供应器, + * 尝试打开其目标机器的 GUI。 + */ +public class CraftingMonitorJumpC2SPacket { + private final AEKey what; + + public CraftingMonitorJumpC2SPacket(AEKey what) { + this.what = what; + } + + public static void encode(CraftingMonitorJumpC2SPacket msg, FriendlyByteBuf buf) { + AEKey.writeKey(buf, msg.what); + } + + public static CraftingMonitorJumpC2SPacket decode(FriendlyByteBuf buf) { + AEKey key = AEKey.readKey(buf); + return new CraftingMonitorJumpC2SPacket(key); + } + + public static void handle(CraftingMonitorJumpC2SPacket msg, Supplier ctx) { + NetworkEvent.Context context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) return; + + LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorJumpC2SPacket key={} from {}", msg.what, player.getGameProfile().getName()); + + // 必须在 CraftingCPU 界面内 + if (!(player.containerMenu instanceof appeng.menu.me.crafting.CraftingCPUMenu menu)) { + LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort"); + return; + } + + // 通过菜单 target(可能是 BlockEntity/Part/ItemHost)按 IActionHost 获取 Grid + IGrid grid = null; + Object target = ((appeng.menu.AEBaseMenu) menu).getTarget(); + if (target instanceof IActionHost host && host.getActionableNode() != null) { + grid = host.getActionableNode().getGrid(); + } + if (grid == null) { + LogUtils.getLogger().info("EAP[S]: grid is null, abort"); + return; + } + + var cs = grid.getCraftingService(); + if (!(cs instanceof CraftingService craftingService)) { + LogUtils.getLogger().info("EAP[S]: craftingService is null/unsupported, abort"); + return; + } + + // 1) 根据 AEKey 找到可能的样板(pattern) + Collection patterns = craftingService.getCraftingFor(msg.what); + LogUtils.getLogger().info("EAP[S]: patterns found={} for key={}", patterns.size(), msg.what); + if (patterns.isEmpty()) { + return; + } + + // 2) 遍历提供该样板的 Provider,优先 PatternProviderLogic + for (var pattern : patterns) { + var providers = craftingService.getProviders(pattern); + int providerCount = 0; + for (var provider : providers) { + providerCount++; + try { + LogUtils.getLogger().info("EAP[S]: provider class={}", provider.getClass().getName()); + } catch (Throwable ignored) {} + if (provider instanceof PatternProviderLogic ppl) { + // 使用 accessor 获取 host(受保护字段通过 accessor 访问) + PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host(); + if (host == null) continue; + var pbe = host.getBlockEntity(); + ServerLevel serverLevel = player.serverLevel(); + + // 尝试对邻居打开 GUI(复用 OpenProviderUiC2SPacket 的策略) + for (Direction dir : host.getTargets()) { + BlockPos targetPos = pbe.getBlockPos().relative(dir); + var tbe = serverLevel.getBlockEntity(targetPos); + if (tbe instanceof MenuProvider provider1) { + LogUtils.getLogger().info("EAP[S]: open screen via MenuProvider at {}", targetPos); + NetworkHooks.openScreen(player, provider1, targetPos); + context.setPacketHandled(true); + return; + } + var tstate = serverLevel.getBlockState(targetPos); + var provider2 = tstate.getMenuProvider(serverLevel, targetPos); + if (provider2 != null) { + LogUtils.getLogger().info("EAP[S]: open screen via state.getMenuProvider at {}", targetPos); + NetworkHooks.openScreen(player, provider2, targetPos); + context.setPacketHandled(true); + return; + } + } + + // 兜底:若无 MenuProvider,始终模拟一次右键(优先有方块实体的一面) + InteractionHand hand = player.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.MAIN_HAND; + Direction chosen = null; + for (Direction d : host.getTargets()) { + if (serverLevel.getBlockEntity(pbe.getBlockPos().relative(d)) != null) { chosen = d; break; } + } + if (chosen == null) { + for (Direction d : host.getTargets()) { + if (!serverLevel.getBlockState(pbe.getBlockPos().relative(d)).isAir()) { chosen = d; break; } + } + } + if (chosen != null) { + BlockPos targetPos = pbe.getBlockPos().relative(chosen); + var state2 = serverLevel.getBlockState(targetPos); + var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false); + InteractionResult r = state2.use(serverLevel, player, hand, hit); + LogUtils.getLogger().info("EAP[S]: simulated use on {}, face={}, result={}", targetPos, chosen, r); + if (r.consumesAction()) { + context.setPacketHandled(true); + return; + } + } + } + } + LogUtils.getLogger().info("EAP[S]: providers count for one pattern: {}", providerCount); + } + LogUtils.getLogger().info("EAP[S]: no target opened for key={}", msg.what); + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java new file mode 100644 index 0000000..216546c --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java @@ -0,0 +1,115 @@ +package com.extendedae_plus.network; + +import appeng.api.crafting.IPatternDetails; +import appeng.api.networking.IGrid; +import appeng.api.networking.security.IActionHost; +import appeng.api.stacks.AEKey; +import appeng.helpers.patternprovider.PatternProviderLogic; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import appeng.me.service.CraftingService; +import appeng.menu.AEBaseMenu; +import appeng.menu.me.crafting.CraftingCPUMenu; +import appeng.menu.locator.MenuLocators; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; +import com.mojang.logging.LogUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * 客户端从 CraftingCPUScreen 发送:鼠标下条目对应的 AEKey。 + * 服务端在当前打开的 CraftingCPUMenu 所属网络中,定位匹配该 AEKey 的样板供应器, + * 打开该供应器自身的 UI(不是目标机器的 UI)。 + */ +public class CraftingMonitorOpenProviderC2SPacket { + private final AEKey what; + + public CraftingMonitorOpenProviderC2SPacket(AEKey what) { + this.what = what; + } + + public static void encode(CraftingMonitorOpenProviderC2SPacket msg, FriendlyByteBuf buf) { + AEKey.writeKey(buf, msg.what); + } + + public static CraftingMonitorOpenProviderC2SPacket decode(FriendlyByteBuf buf) { + AEKey key = AEKey.readKey(buf); + return new CraftingMonitorOpenProviderC2SPacket(key); + } + + public static void handle(CraftingMonitorOpenProviderC2SPacket msg, Supplier ctx) { + NetworkEvent.Context context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) return; + + LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorOpenProviderC2SPacket key={} from {}", msg.what, player.getGameProfile().getName()); + + // 必须在 CraftingCPU 界面内 + if (!(player.containerMenu instanceof CraftingCPUMenu menu)) { + LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort"); + return; + } + + // 通过菜单的 target(可能是 BlockEntity/Part/ItemHost),按 IActionHost 获取 Grid + IGrid grid = null; + Object target = ((AEBaseMenu) menu).getTarget(); + if (target instanceof IActionHost host && host.getActionableNode() != null) { + grid = host.getActionableNode().getGrid(); + } + if (grid == null) { + LogUtils.getLogger().info("EAP[S]: grid is null, abort"); + return; + } + + var cs = grid.getCraftingService(); + if (!(cs instanceof CraftingService craftingService)) { + LogUtils.getLogger().info("EAP[S]: craftingService is null/unsupported, abort"); + return; + } + + // 1) 根据 AEKey 找到可能的样板(pattern) + Collection patterns = craftingService.getCraftingFor(msg.what); + LogUtils.getLogger().info("EAP[S]: patterns found={} for key={}", patterns.size(), msg.what); + if (patterns.isEmpty()) { + return; + } + + // 2) 遍历提供该样板的 Provider,定位 PatternProviderLogic + for (var pattern : patterns) { + var providers = craftingService.getProviders(pattern); + for (var provider : providers) { + if (provider instanceof PatternProviderLogic ppl) { + // accessor 获取 host + PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host(); + if (host == null) continue; + var pbe = host.getBlockEntity(); + if (pbe == null) continue; + // 在服务端上下文中执行,pbe 仅用于构造菜单定位器 + + // 直接打开供应器自身的 UI(调用 Host 默认方法) + try { + // 部件与方块实体分别选择定位器 + if (host instanceof appeng.parts.AEBasePart part) { + host.openMenu(player, MenuLocators.forPart(part)); + } else { + host.openMenu(player, MenuLocators.forBlockEntity(pbe)); + } + context.setPacketHandled(true); + return; + } catch (Throwable t) { + LogUtils.getLogger().error("EAP[S]: open provider UI failed at {}", pbe.getBlockPos(), t); + } + } + } + } + + LogUtils.getLogger().info("EAP[S]: no provider UI opened for key={}", msg.what); + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/network/ModNetwork.java b/src/main/java/com/extendedae_plus/network/ModNetwork.java index 3aad8ea..3cfe50a 100644 --- a/src/main/java/com/extendedae_plus/network/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/network/ModNetwork.java @@ -18,6 +18,12 @@ public class ModNetwork { private static int id = 0; public static void register() { + CHANNEL.messageBuilder(OpenProviderUiC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(OpenProviderUiC2SPacket::encode) + .decoder(OpenProviderUiC2SPacket::decode) + .consumerNetworkThread(OpenProviderUiC2SPacket::handle) + .add(); + CHANNEL.messageBuilder(PickFromWirelessC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) .encoder(PickFromWirelessC2SPacket::encode) .decoder(PickFromWirelessC2SPacket::decode) @@ -65,6 +71,18 @@ public class ModNetwork { .decoder(AdvancedBlockingSyncS2CPacket::decode) .consumerNetworkThread(AdvancedBlockingSyncS2CPacket::handle) .add(); + + CHANNEL.messageBuilder(CraftingMonitorJumpC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(CraftingMonitorJumpC2SPacket::encode) + .decoder(CraftingMonitorJumpC2SPacket::decode) + .consumerNetworkThread(CraftingMonitorJumpC2SPacket::handle) + .add(); + + CHANNEL.messageBuilder(CraftingMonitorOpenProviderC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(CraftingMonitorOpenProviderC2SPacket::encode) + .decoder(CraftingMonitorOpenProviderC2SPacket::decode) + .consumerNetworkThread(CraftingMonitorOpenProviderC2SPacket::handle) + .add(); } private static int nextId() { return id++; } diff --git a/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java b/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java new file mode 100644 index 0000000..8a02170 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java @@ -0,0 +1,133 @@ +package com.extendedae_plus.network; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.core.Direction; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.InteractionResult; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.Level; +import net.minecraftforge.network.NetworkEvent; +import net.minecraftforge.network.NetworkHooks; + +import java.util.function.Supplier; + +public class OpenProviderUiC2SPacket { + private final long posLong; + private final ResourceLocation dimId; + private final int faceOrd; // 目前保留,若目标需要可用 + + public OpenProviderUiC2SPacket(long posLong, ResourceLocation dimId, int faceOrd) { + this.posLong = posLong; + this.dimId = dimId; + this.faceOrd = faceOrd; + } + + public static void encode(OpenProviderUiC2SPacket msg, FriendlyByteBuf buf) { + buf.writeLong(msg.posLong); + buf.writeResourceLocation(msg.dimId); + buf.writeVarInt(msg.faceOrd); + } + + public static OpenProviderUiC2SPacket decode(FriendlyByteBuf buf) { + long posLong = buf.readLong(); + ResourceLocation dimId = buf.readResourceLocation(); + int faceOrd = buf.readVarInt(); + return new OpenProviderUiC2SPacket(posLong, dimId, faceOrd); + + } + + public static void handle(OpenProviderUiC2SPacket msg, Supplier ctx) { + NetworkEvent.Context context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) return; + + + // 校验维度与方块 + ResourceKey levelKey = ResourceKey.create(Registries.DIMENSION, msg.dimId); + ServerLevel level = player.server.getLevel(levelKey); + if (level == null) { + return; // 无效维度 + } + + BlockPos pos = BlockPos.of(msg.posLong); + if (!level.isLoaded(pos)) { + return; // 区块未加载 + } + + var be = level.getBlockEntity(pos); + var stateAtPos = level.getBlockState(pos); + + // 目标通常是供应器所面对/连接的“相邻方块”,优先尝试邻居 + Direction[] tries = (msg.faceOrd >= 0 && msg.faceOrd < Direction.values().length) + ? new Direction[]{Direction.values()[msg.faceOrd]} + : Direction.values(); + + for (Direction dir : tries) { + BlockPos targetPos = pos.relative(dir); + BlockEntity tbe = level.getBlockEntity(targetPos); + if (tbe instanceof MenuProvider provider) { + NetworkHooks.openScreen(player, provider, targetPos); + return; + } + var tstate = level.getBlockState(targetPos); + MenuProvider provider2 = tstate.getMenuProvider(level, targetPos); + if (provider2 != null) { + NetworkHooks.openScreen(player, provider2, targetPos); + return; + } + } + + // 如果邻居也未提供 MenuProvider,则兜底:尽量模拟一次徒手右键相邻方块 + boolean anyHandEmpty = player.getMainHandItem().isEmpty() || player.getOffhandItem().isEmpty(); + if (anyHandEmpty) { + InteractionHand hand = player.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND; + if (msg.faceOrd >= 0 && msg.faceOrd < Direction.values().length) { + Direction dir = Direction.values()[msg.faceOrd]; + BlockPos targetPos = pos.relative(dir); + var state2 = level.getBlockState(targetPos); + var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), dir.getOpposite(), targetPos, false); + InteractionResult r = state2.use(level, player, hand, hit); + if (r.consumesAction()) { + return; + } + } else { + // 无明确朝向:优先挑选有方块实体的邻居,否则挑选非空气方块 + Direction chosen = null; + for (Direction d : Direction.values()) { + if (level.getBlockEntity(pos.relative(d)) != null) { chosen = d; break; } + } + if (chosen == null) { + for (Direction d : Direction.values()) { + if (!level.getBlockState(pos.relative(d)).isAir()) { chosen = d; break; } + } + } + if (chosen != null) { + BlockPos targetPos = pos.relative(chosen); + var state2 = level.getBlockState(targetPos); + var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false); + InteractionResult r = state2.use(level, player, hand, hit); + if (r.consumesAction()) { + return; + } + } else { + // 无可选邻居 + } + } + } else { + // 双手占用则跳过兜底交互 + } + + context.setPacketHandled(true); + }); + } +} diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index 52f97e1..7a916e8 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -7,7 +7,6 @@ "PickFromWirelessMixin", "accessor.AbstractContainerScreenAccessor", "accessor.ScreenAccessor", - "accessor.ScreenInvoker", "ae2.AEBaseScreenMixin", "ae2.PatternEncodingTermScreenMixin", "ae2.PatternProviderScreenMixin",