diff --git a/build.gradle b/build.gradle index d31f0f1..64544ed 100644 --- a/build.gradle +++ b/build.gradle @@ -102,9 +102,6 @@ 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" } allprojects { diff --git a/gradle.properties b/gradle.properties index d28f3d4..b3b0a9f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G loom.platform = forge # Mod properties -mod_version = 1.3.3 +mod_version = 1.3.4-beta maven_group = com.extendedae_plus archives_name = extendedae_plus 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..0a1cf72 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java @@ -11,6 +11,7 @@ import appeng.client.gui.widgets.IconButton; import appeng.menu.AEBaseMenu; import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; import net.minecraft.client.renderer.Rect2i; import net.minecraft.network.chat.Component; @@ -29,9 +30,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, remap = false) public abstract class GuiExPatternTerminalMixin extends AEBaseScreen { @Unique @@ -46,6 +51,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; @@ -181,6 +194,97 @@ 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); + + long posLong = (long) pos.getClass().getMethod("asLong").invoke(pos); + Object rl = playerWorld.getClass().getMethod("location").invoke(playerWorld); // ResourceLocation + String dimStr = (String) rl.getClass().getMethod("toString").invoke(rl); + int faceOrd = -1; + if (face != null) { + faceOrd = (int) face.getClass().getMethod("ordinal").invoke(face); + } + + // 发送 CGenericPacket("open_ui", [posLong, dim, face]) + try { + Class EPPNetworkHandlerClass = Class.forName("com.glodblock.github.extendedae.network.EPPNetworkHandler"); + Object handlerInstance = EPPNetworkHandlerClass.getField("INSTANCE").get(null); + + Class packetClass = Class.forName("com.glodblock.github.glodium.network.packet.CGenericPacket"); + Constructor constructor = packetClass.getConstructor(String.class, Object[].class); + Object packet = constructor.newInstance("open_ui", new Object[]{posLong, dimStr, faceOrd}); + + Class iMessage = Class.forName("com.glodblock.github.glodium.network.packet.IMessage"); + Method sendToServer = EPPNetworkHandlerClass.getMethod("sendToServer", iMessage); + + sendToServer.invoke(handlerInstance, packet); + if (this.minecraft != null && this.minecraft.player != null) { + EAP_LOGGER.debug("[EPlus] Sent open_ui packet: pos={}, dim={}, face={}", posLong, dimStr, faceOrd); + } + } catch (Throwable t) { + if (this.minecraft != null && this.minecraft.player != null) { + this.minecraft.player.displayClientMessage(Component.literal("❌ ExtendedAE Plus: 网络模块不可用,无法发送打开UI请求"), true); + } + } + } catch (Throwable t) { + EAP_LOGGER.warn("[EPlus] eap$tryOpenProviderUI failed: {}", t.toString()); + } + } + /** * 重置当前选择的样板供应器ID */ @@ -228,6 +332,13 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen this.addToLeftToolbar(this.eap$toggleSlotsButton); } + @Inject(method = "init", at = @At("TAIL"), remap = false) + 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 +347,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 +456,71 @@ 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(); + + if (!eap$debugLoggedOnce) { + EAP_LOGGER.info("[EPlus] GuiExPatternTerminalMixin.afterDrawFG fired: rows={}, currentScroll={}, visibleRows={}", + rows.size(), currentScroll, visibleRows); + eap$debugLoggedOnce = true; + } + + // 先隐藏旧按钮,避免残留 + 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; + + Button btn = eap$openUIButtons.get(rowIndex); + if (btn == null) { + btn = Button.builder(Component.literal("UI"), (b) -> { + eap$tryOpenProviderUI(rowIndex); + }).size(18, 16).build(); + btn.setTooltip(Tooltip.create(Component.literal("打开该供应器目标容器的界面"))); + eap$openUIButtons.put(rowIndex, btn); + this.addRenderableWidget(btn); + } + btn.setPosition(bx, by); + btn.visible = true; + shownCount++; + } + if (shownCount == 0) { + EAP_LOGGER.debug("[EPlus] No GroupHeaderRow visible in current page (scroll={}, rows={})", currentScroll, rows.size()); + } else { + EAP_LOGGER.debug("[EPlus] GroupHeaderRow buttons shown count: {}", shownCount); + } + } catch (Throwable ignored) { + } + // 原有的搜索高亮逻辑 // 仅当任一搜索框非空时绘制叠加层(与原版行为保持一致) boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty())