From 48709c6ce85285d45992b94d7f94e4cd22cd88a0 Mon Sep 17 00:00:00 2001 From: GaLicn <133291877+GaLicn@users.noreply.github.com> Date: Thu, 18 Sep 2025 23:34:16 +0800 Subject: [PATCH] =?UTF-8?q?ME=E6=8E=A5=E5=8F=A3=E5=80=8D=E5=A2=9E=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../extendedae_plus/init/ModCapabilities.java | 9 + .../AbstractContainerScreenAccessor.java | 2 + .../ae2/client/gui/InterfaceScreenMixin.java | 280 ++++++++++++++++++ .../InterfaceAdjustConfigAmountC2SPacket.java | 114 +++++++ .../extendedae_plus/network/ModNetwork.java | 1 + .../resources/extendedae_plus.mixins.json | 1 + 6 files changed, 407 insertions(+) create mode 100644 src/main/java/com/extendedae_plus/mixin/ae2/client/gui/InterfaceScreenMixin.java create mode 100644 src/main/java/com/extendedae_plus/network/InterfaceAdjustConfigAmountC2SPacket.java diff --git a/src/main/java/com/extendedae_plus/init/ModCapabilities.java b/src/main/java/com/extendedae_plus/init/ModCapabilities.java index 363f1aa..2d7aa2e 100644 --- a/src/main/java/com/extendedae_plus/init/ModCapabilities.java +++ b/src/main/java/com/extendedae_plus/init/ModCapabilities.java @@ -25,6 +25,15 @@ public final class ModCapabilities { (be, ctx) -> (IInWorldGridNodeHost) be ); + // 并行处理单元(CraftingUnitBlock -> CraftingBlockEntity 实现了 IInWorldGridNodeHost) + // 未注册该能力时,AE 电缆通过 GridHelper.getNodeHost(...) 无法发现节点,导致节点不入网, + // 方块虽然能成型并提供并行度,但 getMainNode().isOnline() 为 false,从而显示“设备离线”。 + event.registerBlockEntity( + AECapabilities.IN_WORLD_GRID_NODE_HOST, + ModBlockEntities.EPLUS_CRAFTING_UNIT_BE.get(), + (be, ctx) -> (IInWorldGridNodeHost) be + ); + // 如果还有其他实现了 IInWorldGridNodeHost 的方块实体,也在这里一并注册 // event.registerBlockEntity(AECapabilities.IN_WORLD_GRID_NODE_HOST, ModBlockEntities.NETWORK_PATTERN_CONTROLLER_BE.get(), (be, ctx) -> (IInWorldGridNodeHost) be); } diff --git a/src/main/java/com/extendedae_plus/mixin/accessor/AbstractContainerScreenAccessor.java b/src/main/java/com/extendedae_plus/mixin/accessor/AbstractContainerScreenAccessor.java index 27b50a0..ea1eef0 100644 --- a/src/main/java/com/extendedae_plus/mixin/accessor/AbstractContainerScreenAccessor.java +++ b/src/main/java/com/extendedae_plus/mixin/accessor/AbstractContainerScreenAccessor.java @@ -2,6 +2,7 @@ package com.extendedae_plus.mixin.accessor; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.gen.Accessor; @@ -11,4 +12,5 @@ public interface AbstractContainerScreenAccessor { + + @Unique private ActionEPPButton eap$x2Button; + @Unique private ActionEPPButton eap$divideBy2Button; + @Unique private ActionEPPButton eap$x5Button; + @Unique private ActionEPPButton eap$divideBy5Button; + @Unique private ActionEPPButton eap$x10Button; + @Unique private ActionEPPButton eap$divideBy10Button; + + @Unique private int eap$lastLeftPos = -1; + @Unique private int eap$lastTopPos = -1; + @Unique private int eap$lastImageWidth = -1; + @Unique private int eap$lastImageHeight = -1; + // 最近一次在 CONFIG 区域悬停的配置槽索引 + @Unique private int eap$lastConfigIndex = -1; + + @Inject(method = "init", at = @At("TAIL")) + private void eap$addScaleButtons(CallbackInfo ci) { + if (!eap$isSupportedInterfaceScreen()) { + return; + } + try { LogUtils.getLogger().info("[EAP][InterfaceMixin] init tail reached, preparing scale buttons."); } catch (Throwable ignored) {} + if (eap$x2Button == null) { + eap$x2Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(false, 2), NewIcon.MULTIPLY2); + eap$x2Button.setTooltip(null); + eap$x2Button.setVisibility(true); + } + if (eap$divideBy2Button == null) { + eap$divideBy2Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(true, 2), NewIcon.DIVIDE2); + eap$divideBy2Button.setTooltip(null); + eap$divideBy2Button.setVisibility(true); + } + if (eap$x5Button == null) { + eap$x5Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(false, 5), NewIcon.MULTIPLY5); + eap$x5Button.setTooltip(null); + eap$x5Button.setVisibility(true); + } + if (eap$divideBy5Button == null) { + eap$divideBy5Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(true, 5), NewIcon.DIVIDE5); + eap$divideBy5Button.setTooltip(null); + eap$divideBy5Button.setVisibility(true); + } + if (eap$x10Button == null) { + eap$x10Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(false, 10), NewIcon.MULTIPLY10); + eap$x10Button.setTooltip(null); + eap$x10Button.setVisibility(true); + } + if (eap$divideBy10Button == null) { + eap$divideBy10Button = new ActionEPPButton((b) -> eap$sendAdjustForAllConfigs(true, 10), NewIcon.DIVIDE10); + eap$divideBy10Button.setTooltip(null); + eap$divideBy10Button.setVisibility(true); + } + + // 注册到渲染与交互列表 + var accessor = (ScreenAccessor) (Object) this; + if (!accessor.eap$getRenderables().contains(eap$divideBy2Button)) accessor.eap$getRenderables().add(eap$divideBy2Button); + if (!accessor.eap$getChildren().contains(eap$divideBy2Button)) accessor.eap$getChildren().add(eap$divideBy2Button); + if (!accessor.eap$getRenderables().contains(eap$x2Button)) accessor.eap$getRenderables().add(eap$x2Button); + if (!accessor.eap$getChildren().contains(eap$x2Button)) accessor.eap$getChildren().add(eap$x2Button); + if (!accessor.eap$getRenderables().contains(eap$divideBy5Button)) accessor.eap$getRenderables().add(eap$divideBy5Button); + if (!accessor.eap$getChildren().contains(eap$divideBy5Button)) accessor.eap$getChildren().add(eap$divideBy5Button); + if (!accessor.eap$getRenderables().contains(eap$x5Button)) accessor.eap$getRenderables().add(eap$x5Button); + if (!accessor.eap$getChildren().contains(eap$x5Button)) accessor.eap$getChildren().add(eap$x5Button); + if (!accessor.eap$getRenderables().contains(eap$divideBy10Button)) accessor.eap$getRenderables().add(eap$divideBy10Button); + if (!accessor.eap$getChildren().contains(eap$divideBy10Button)) accessor.eap$getChildren().add(eap$divideBy10Button); + if (!accessor.eap$getRenderables().contains(eap$x10Button)) accessor.eap$getRenderables().add(eap$x10Button); + if (!accessor.eap$getChildren().contains(eap$x10Button)) accessor.eap$getChildren().add(eap$x10Button); + + eap$relayoutButtons(); + try { LogUtils.getLogger().info("[EAP][InterfaceMixin] buttons added and laid out."); } catch (Throwable ignored) {} + } + + @Inject(method = "containerTick", at = @At("TAIL")) + private void eap$ensureButtons(CallbackInfo ci) { + if (!eap$isSupportedInterfaceScreen()) { + return; + } + var accessor = (ScreenAccessor) (Object) this; + if (eap$divideBy2Button != null && !accessor.eap$getRenderables().contains(eap$divideBy2Button)) { + accessor.eap$getRenderables().add(eap$divideBy2Button); + accessor.eap$getChildren().add(eap$divideBy2Button); + } + if (eap$x2Button != null && !accessor.eap$getRenderables().contains(eap$x2Button)) { + accessor.eap$getRenderables().add(eap$x2Button); + accessor.eap$getChildren().add(eap$x2Button); + } + if (eap$divideBy5Button != null && !accessor.eap$getRenderables().contains(eap$divideBy5Button)) { + accessor.eap$getRenderables().add(eap$divideBy5Button); + accessor.eap$getChildren().add(eap$divideBy5Button); + } + if (eap$x5Button != null && !accessor.eap$getRenderables().contains(eap$x5Button)) { + accessor.eap$getRenderables().add(eap$x5Button); + accessor.eap$getChildren().add(eap$x5Button); + } + if (eap$divideBy10Button != null && !accessor.eap$getRenderables().contains(eap$divideBy10Button)) { + accessor.eap$getRenderables().add(eap$divideBy10Button); + accessor.eap$getChildren().add(eap$divideBy10Button); + } + if (eap$x10Button != null && !accessor.eap$getRenderables().contains(eap$x10Button)) { + accessor.eap$getRenderables().add(eap$x10Button); + accessor.eap$getChildren().add(eap$x10Button); + } + + int curLeft = ((AbstractContainerScreenAccessor) (Object) this).eap$getLeftPos(); + int curTop = ((AbstractContainerScreenAccessor) (Object) this).eap$getTopPos(); + int curImgW = ((AbstractContainerScreenAccessor) (Object) this).eap$getImageWidth(); + int curImgH = ((AbstractContainerScreenAccessor) (Object) this).eap$getImageHeight(); + if (curLeft != eap$lastLeftPos || curTop != eap$lastTopPos || curImgW != eap$lastImageWidth || curImgH != eap$lastImageHeight) { + eap$lastLeftPos = curLeft; + eap$lastTopPos = curTop; + eap$lastImageWidth = curImgW; + eap$lastImageHeight = curImgH; + eap$relayoutButtons(); + } + eap$updateLastConfigFromHover(); + } + + @Unique + private void eap$sendAdjustForHoveredConfig(boolean divide, int factor) { + try { + if (!eap$isSupportedInterfaceScreen()) { + return; + } + Slot hovered = ((AbstractContainerScreenAccessor) (Object) this).eap$getHoveredSlot(); + var screen = (AEBaseScreen) (Object) this; + var menu = screen.getMenu(); + if (!(menu instanceof appeng.menu.implementations.InterfaceMenu interfaceMenu)) { + return; + } + var configSlots = interfaceMenu.getSlots(SlotSemantics.CONFIG); + if (configSlots == null || configSlots.isEmpty()) { + return; + } + + Integer slotFieldObj = null; + if (hovered != null) { + for (var s : configSlots) { + if (s == hovered) { + try { + var f = s.getClass().getDeclaredField("slot"); + f.setAccessible(true); + Object v = f.get(s); + if (v instanceof Integer i) { + slotFieldObj = i; + } + } catch (Throwable ignored) {} + if (slotFieldObj == null) { + slotFieldObj = configSlots.indexOf(s); + } + break; + } + } + } + int slotField = -1; + if (slotFieldObj != null) { + slotField = slotFieldObj; + } else if (eap$lastConfigIndex >= 0 && eap$lastConfigIndex < configSlots.size()) { + slotField = eap$lastConfigIndex; + } + if (slotField < 0) { + return; + } + + var conn = Minecraft.getInstance().getConnection(); + if (conn != null) conn.send(new InterfaceAdjustConfigAmountC2SPacket(slotField, divide, factor)); + } catch (Throwable ignored) {} + } + + @Unique + private void eap$sendAdjustForAllConfigs(boolean divide, int factor) { + try { + if (!eap$isSupportedInterfaceScreen()) { + return; + } + var conn = Minecraft.getInstance().getConnection(); + if (conn != null) conn.send(new InterfaceAdjustConfigAmountC2SPacket(-1, divide, factor)); + } catch (Throwable ignored) {} + } + + @Unique + private boolean eap$isSupportedInterfaceScreen() { + if (((Object) this) instanceof InterfaceScreen) { + return true; + } + try { + String cn = ((Object) this).getClass().getName(); + if ("com.glodblock.github.extendedae.client.gui.GuiExInterface".equals(cn)) { + return true; + } + } catch (Throwable ignored) {} + return false; + } + + @Unique + private void eap$relayoutButtons() { + try { + int leftPos = ((AbstractContainerScreenAccessor) (Object) this).eap$getLeftPos(); + int topPos = ((AbstractContainerScreenAccessor) (Object) this).eap$getTopPos(); + int imageWidth = ((AbstractContainerScreenAccessor) (Object) this).eap$getImageWidth(); + int bx = leftPos + imageWidth + 1; + int by = topPos + 20; + int spacing = 22; + if (eap$divideBy2Button != null) { eap$divideBy2Button.setX(bx); eap$divideBy2Button.setY(by); } + if (eap$x2Button != null) { eap$x2Button.setX(bx); eap$x2Button.setY(by + spacing); } + if (eap$divideBy5Button != null) { eap$divideBy5Button.setX(bx); eap$divideBy5Button.setY(by + spacing * 2); } + if (eap$x5Button != null) { eap$x5Button.setX(bx); eap$x5Button.setY(by + spacing * 3); } + if (eap$divideBy10Button != null) { eap$divideBy10Button.setX(bx); eap$divideBy10Button.setY(by + spacing * 4); } + if (eap$x10Button != null) { eap$x10Button.setX(bx); eap$x10Button.setY(by + spacing * 5); } + } catch (Throwable ignored) {} + } + + @Unique + private void eap$updateLastConfigFromHover() { + try { + if (!(((Object) this) instanceof InterfaceScreen)) { + return; + } + Slot hovered = ((AbstractContainerScreenAccessor) (Object) this).eap$getHoveredSlot(); + if (hovered == null) { + return; + } + var screen = (AEBaseScreen) (Object) this; + var menu = screen.getMenu(); + if (!(menu instanceof appeng.menu.implementations.InterfaceMenu interfaceMenu)) { + return; + } + var configSlots = interfaceMenu.getSlots(SlotSemantics.CONFIG); + if (configSlots == null || configSlots.isEmpty()) { + return; + } + Integer idx = null; + for (var s : configSlots) { + if (s == hovered) { + try { + var f = s.getClass().getDeclaredField("slot"); + f.setAccessible(true); + Object v = f.get(s); + if (v instanceof Integer i) { + idx = i; + } + } catch (Throwable ignored) {} + if (idx == null) { + idx = configSlots.indexOf(s); + } + break; + } + } + if (idx != null && idx >= 0) { + if (eap$lastConfigIndex != idx) { + eap$lastConfigIndex = idx; + } + } + } catch (Throwable ignored) {} + } +} diff --git a/src/main/java/com/extendedae_plus/network/InterfaceAdjustConfigAmountC2SPacket.java b/src/main/java/com/extendedae_plus/network/InterfaceAdjustConfigAmountC2SPacket.java new file mode 100644 index 0000000..3cd1d17 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/InterfaceAdjustConfigAmountC2SPacket.java @@ -0,0 +1,114 @@ +package com.extendedae_plus.network; + +import appeng.menu.implementations.InterfaceMenu; +import appeng.api.stacks.GenericStack; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +/** + * C2S:调整 ME 接口配置槽位(标记物品)的数量。 + * 支持按因子倍增或整除,且保持最小值为 1。 + */ +public class InterfaceAdjustConfigAmountC2SPacket implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(com.extendedae_plus.ExtendedAEPlus.MODID, "interface_adjust_config_amount")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeVarInt(pkt.slotIndex); + buf.writeBoolean(pkt.divide); + buf.writeVarInt(pkt.factor); + }, + buf -> new InterfaceAdjustConfigAmountC2SPacket(buf.readVarInt(), buf.readBoolean(), buf.readVarInt()) + ); + + private final int slotIndex; // 配置槽位索引(-1 表示全部) + private final boolean divide; // true 表示做除法,否则做乘法 + private final int factor; // 因子:2/5/10 + + public InterfaceAdjustConfigAmountC2SPacket(int slotIndex, boolean divide, int factor) { + this.slotIndex = slotIndex; + this.divide = divide; + this.factor = factor; + } + + @Override + public Type type() { + return TYPE; + } + + public static void handle(final InterfaceAdjustConfigAmountC2SPacket msg, final IPayloadContext ctx) { + ctx.enqueueWork(() -> { + if (!(ctx.player() instanceof ServerPlayer player)) return; + + // 支持 AE2 原版接口和 ExtendedAE 扩展接口(若存在) + InterfaceMenu menu = null; + com.glodblock.github.extendedae.container.ContainerExInterface exMenu = null; + if (player.containerMenu instanceof InterfaceMenu im) { + menu = im; + } else if (player.containerMenu instanceof com.glodblock.github.extendedae.container.ContainerExInterface cem) { + exMenu = cem; + } else { + return; + } + + try { + var logic = (menu != null ? menu.getHost() : exMenu.getHost()).getInterfaceLogic(); + var config = logic.getConfig(); + if (msg.slotIndex == -1) { + // 对所有配置槽统一生效 + int size = config.size(); + for (int idx = 0; idx < size; idx++) { + var st = config.getStack(idx); + if (st == null) continue; + + long current = st.amount(); + int factor = Math.max(1, msg.factor); + long next; + if (msg.divide) { + if (factor <= 1) continue; + if (current % factor != 0) continue; + next = current / factor; + if (next < 1) next = 1; + } else { + if (factor <= 1) continue; + next = current * factor; + if (next < 1) next = 1; + } + + GenericStack newStack = new GenericStack(st.what(), next); + config.setStack(idx, newStack); + } + } else { + var stack = config.getStack(msg.slotIndex); + if (stack == null) return; // 槽位无标记 + + long current = stack.amount(); + int factor = Math.max(1, msg.factor); + long next; + if (msg.divide) { + // 只能整除,且至少为 1 + if (factor <= 1) return; + if (current % factor != 0) return; // 不能整除则跳过 + next = current / factor; + if (next < 1) next = 1; + } else { + // 倍增,至少为 1 + if (factor <= 1) return; + next = current * factor; + if (next < 1) next = 1; + } + + // 应用 + GenericStack newStack = new GenericStack(stack.what(), next); + config.setStack(msg.slotIndex, newStack); + } + // InterfaceLogic.config 的变更监听器会触发保存与计划更新 + } catch (Throwable ignored) {} + }); + } +} diff --git a/src/main/java/com/extendedae_plus/network/ModNetwork.java b/src/main/java/com/extendedae_plus/network/ModNetwork.java index b972155..6a57257 100644 --- a/src/main/java/com/extendedae_plus/network/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/network/ModNetwork.java @@ -10,6 +10,7 @@ public class ModNetwork { registrar.playToServer(ToggleAdvancedBlockingC2SPacket.TYPE, ToggleAdvancedBlockingC2SPacket.STREAM_CODEC, ToggleAdvancedBlockingC2SPacket::handle); registrar.playToServer(ToggleSmartDoublingC2SPacket.TYPE, ToggleSmartDoublingC2SPacket.STREAM_CODEC, ToggleSmartDoublingC2SPacket::handle); registrar.playToServer(ScalePatternsC2SPacket.TYPE, ScalePatternsC2SPacket.STREAM_CODEC, ScalePatternsC2SPacket::handle); + registrar.playToServer(InterfaceAdjustConfigAmountC2SPacket.TYPE, InterfaceAdjustConfigAmountC2SPacket.STREAM_CODEC, InterfaceAdjustConfigAmountC2SPacket::handle); registrar.playToClient(SetPatternHighlightS2CPacket.TYPE, SetPatternHighlightS2CPacket.STREAM_CODEC, SetPatternHighlightS2CPacket::handle); registrar.playToClient(AdvancedBlockingSyncS2CPacket.TYPE, AdvancedBlockingSyncS2CPacket.STREAM_CODEC, AdvancedBlockingSyncS2CPacket::handle); registrar.playToClient(ProvidersListS2CPacket.TYPE, ProvidersListS2CPacket.STREAM_CODEC, ProvidersListS2CPacket::handle); diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index ebb9155..451920a 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -41,6 +41,7 @@ "ae2.accessor.MEStorageScreenAccessor", "ae2.accessor.PatternAccessTermScreenAccessor", "ae2.accessor.PatternAccessTermScreenSlotsRowAccessor", + "ae2.client.gui.InterfaceScreenMixin", "ae2.client.gui.AEBaseScreenMixin", "ae2.client.gui.PatternEncodingTermScreenMixin", "ae2.client.gui.PatternProviderCloseMixin",