diff --git a/src/main/java/com/extendedae_plus/bridge/CompatUpgradeProvider.java b/src/main/java/com/extendedae_plus/bridge/CompatUpgradeProvider.java new file mode 100644 index 0000000..a221c7e --- /dev/null +++ b/src/main/java/com/extendedae_plus/bridge/CompatUpgradeProvider.java @@ -0,0 +1,10 @@ +package com.extendedae_plus.bridge; + +import appeng.api.upgrades.IUpgradeInventory; + +/** + * 仅用于我方在未安装 appflux 时向逻辑类暴露自带升级槽,避免与 appflux 的 IUpgradeableObject 冲突。 + */ +public interface CompatUpgradeProvider { + IUpgradeInventory eap$getCompatUpgrades(); +} diff --git a/src/main/java/com/extendedae_plus/bridge/IUpgradableMenu.java b/src/main/java/com/extendedae_plus/bridge/IUpgradableMenu.java new file mode 100644 index 0000000..9599172 --- /dev/null +++ b/src/main/java/com/extendedae_plus/bridge/IUpgradableMenu.java @@ -0,0 +1,12 @@ +package com.extendedae_plus.bridge; + +import appeng.menu.ToolboxMenu; + +/** + * 提供给 AE2 菜单以暴露升级槽与工具箱面板的接口。 + * PatternProviderMenu 的 mixin 会在未安装 appflux 时实现该接口, + * 以便在界面上显示升级卡槽(用于放置频道卡)。 + */ +public interface IUpgradableMenu { + ToolboxMenu getToolbox(); +} diff --git a/src/main/java/com/extendedae_plus/compat/UpgradeSlotCompat.java b/src/main/java/com/extendedae_plus/compat/UpgradeSlotCompat.java new file mode 100644 index 0000000..0c9f191 --- /dev/null +++ b/src/main/java/com/extendedae_plus/compat/UpgradeSlotCompat.java @@ -0,0 +1,38 @@ +package com.extendedae_plus.compat; + +import net.neoforged.fml.loading.FMLLoader; +import net.neoforged.fml.ModList; + +public final class UpgradeSlotCompat { + private static Boolean APPFLUX_LOADED; + + private UpgradeSlotCompat() {} + + private static boolean isAppfluxLoaded() { + if (APPFLUX_LOADED == null) { + try { + APPFLUX_LOADED = ModList.get().isLoaded("appflux"); + } catch (Throwable t) { + // 早期阶段或运行环境差异 + APPFLUX_LOADED = false; + } + } + return APPFLUX_LOADED; + } + + // 是否由我们提供升级槽(当未安装 appflux 时) + public static boolean shouldEnableUpgradeSlots() { + return !isAppfluxLoaded(); + } + + // 是否启用频道卡支持(两种情况下都启用) + public static boolean shouldEnableChannelCard() { + return true; + } + + // 客户端界面是否需要显示升级面板(装/不装 appflux 均显示; + // appflux 提供的升级槽会以 SlotSemantics.UPGRADE 出现在菜单中,我们只负责渲染面板) + public static boolean shouldAddUpgradePanelToScreen() { + return true; + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderScreenUpgradesMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderScreenUpgradesMixin.java new file mode 100644 index 0000000..d8b60fb --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderScreenUpgradesMixin.java @@ -0,0 +1,67 @@ +package com.extendedae_plus.mixin.ae2.client.gui; + +import appeng.api.upgrades.Upgrades; +import appeng.client.gui.AEBaseScreen; +import appeng.client.gui.implementations.PatternProviderScreen; +import appeng.client.gui.style.ScreenStyle; +import appeng.client.gui.widgets.ToolboxPanel; +import appeng.client.gui.widgets.UpgradesPanel; +import appeng.core.localization.GuiText; +import appeng.menu.SlotSemantics; +import appeng.menu.implementations.PatternProviderMenu; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import appeng.menu.AEBaseMenu; +import com.extendedae_plus.compat.UpgradeSlotCompat; +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; + +@Mixin(value = PatternProviderScreen.class, priority = 2000, remap = false) +public abstract class PatternProviderScreenUpgradesMixin extends AEBaseScreen { + + @Inject(method = "", at = @At("TAIL")) + private void eap$initUpgrades(PatternProviderMenu menu, Inventory playerInventory, Component title, ScreenStyle style, CallbackInfo ci) { + if (!UpgradeSlotCompat.shouldAddUpgradePanelToScreen()) { + return; + } + try { + this.widgets.add("upgrades", new UpgradesPanel( + menu.getSlots(SlotSemantics.UPGRADE), + this::eap$getCompatibleUpgrades)); + } catch (IllegalStateException already) { + // 已存在同名面板(可能由 AE2 或其他模组添加),忽略 + com.extendedae_plus.util.ExtendedAELogger.LOGGER.debug("[样板供应器][界面] 升级面板已存在,跳过添加"); + } + if (menu instanceof AEBaseMenu base && base instanceof com.extendedae_plus.bridge.IUpgradableMenu upg && upg.getToolbox() != null && upg.getToolbox().isPresent()) { + try { + this.widgets.add("toolbox", new ToolboxPanel(style, upg.getToolbox().getName())); + } catch (IllegalStateException already) { + com.extendedae_plus.util.ExtendedAELogger.LOGGER.debug("[样板供应器][界面] 工具箱面板已存在,跳过添加"); + } + } + } + + @Unique + private List eap$getCompatibleUpgrades() { + var list = new ArrayList(); + list.add(GuiText.CompatibleUpgrades.text()); + if (menu instanceof AEBaseMenu base) { + var target = base.getTarget(); + if (target instanceof PatternProviderLogicHost host) { + list.addAll(Upgrades.getTooltipLinesForMachine(host.getTerminalIcon().getItem())); + } + } + return list; + } + + public PatternProviderScreenUpgradesMixin(C menu, Inventory playerInventory, Component title, ScreenStyle style) { + super(menu, playerInventory, title, style); + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/compat/PatternProviderLogicCompatMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/compat/PatternProviderLogicCompatMixin.java new file mode 100644 index 0000000..d9a530b --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/compat/PatternProviderLogicCompatMixin.java @@ -0,0 +1,280 @@ +package com.extendedae_plus.mixin.ae2.compat; + +import appeng.api.networking.IManagedGridNode; +import appeng.api.networking.security.IActionSource; +import appeng.api.upgrades.IUpgradeInventory; +import appeng.api.upgrades.IUpgradeableObject; +import appeng.api.upgrades.UpgradeInventories; +import appeng.helpers.patternprovider.PatternProviderLogic; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import com.extendedae_plus.ae.items.ChannelCardItem; +import com.extendedae_plus.bridge.InterfaceWirelessLinkBridge; +import com.extendedae_plus.bridge.CompatUpgradeProvider; +import com.extendedae_plus.compat.UpgradeSlotCompat; +import com.extendedae_plus.init.ModItems; +import com.extendedae_plus.wireless.WirelessSlaveLink; +import com.extendedae_plus.wireless.endpoint.GenericNodeEndpointImpl; +import com.extendedae_plus.util.ExtendedAELogger; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * 样板供应器频道卡兼容实现: + * - 未安装 appflux 时,提供 1 个升级槽并读取频道卡; + * - 安装 appflux 时,优先从 appflux 提供的升级槽读取频道卡; + * - 建立到无线主站的网格连接。 + */ +@Mixin(value = PatternProviderLogic.class, remap = false) +public abstract class PatternProviderLogicCompatMixin implements CompatUpgradeProvider, InterfaceWirelessLinkBridge { + + @Unique + private IUpgradeInventory eap$compatUpgrades = UpgradeInventories.empty(); + + @Unique + private WirelessSlaveLink eap$compatLink; + + @Unique + private long eap$compatLastChannel = -1; + + @Unique + private boolean eap$compatClientConnected = false; + + @Unique + private boolean eap$compatHasInitialized = false; + + @Unique + private int eap$compatDelayedInitTicks = 0; + + @Final + @Shadow + private PatternProviderLogicHost host; + + @Final + @Shadow + private IManagedGridNode mainNode; + + @Final + @Shadow + private IActionSource actionSource; + + @Inject(method = "(Lappeng/api/networking/IManagedGridNode;Lappeng/helpers/patternprovider/PatternProviderLogicHost;I)V", + at = @At("TAIL")) + private void eap$compatInit(IManagedGridNode mainNode, PatternProviderLogicHost host, int size, CallbackInfo ci) { + try { + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + this.eap$compatUpgrades = UpgradeInventories.forMachine( + host.getTerminalIcon().getItem(), 1, this::eap$compatOnUpgradesChanged); + ExtendedAELogger.LOGGER.debug("[样板供应器] 初始化自带升级槽 (未安装 appflux)"); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 初始化兼容升级槽失败", t); + } + } + + @Unique + private void eap$compatOnUpgradesChanged() { + try { + this.host.saveChanges(); + eap$compatLastChannel = -1; + eap$compatHasInitialized = false; + ExtendedAELogger.LOGGER.debug("[样板供应器] 升级变更 -> 触发初始化"); + eap$compatInitializeChannelLink(); + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 兼容升级变更处理失败", t); + } + } + + @Inject(method = "writeToNBT", at = @At("TAIL")) + private void eap$compatWrite(CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries, CallbackInfo ci) { + try { + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + this.eap$compatUpgrades.writeToNBT(tag, "compat_upgrades", registries); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 保存兼容升级失败", t); + } + } + + @Inject(method = "readFromNBT", at = @At("TAIL")) + private void eap$compatRead(CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries, CallbackInfo ci) { + try { + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + this.eap$compatUpgrades.readFromNBT(tag, "compat_upgrades", registries); + eap$compatLastChannel = -1; + eap$compatHasInitialized = false; + eap$compatInitializeChannelLink(); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 读取兼容升级失败", t); + } + } + + @Inject(method = "addDrops", at = @At("TAIL")) + private void eap$compatDrops(List drops, CallbackInfo ci) { + try { + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + for (var s : this.eap$compatUpgrades) { + if (!s.isEmpty()) drops.add(s); + } + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 掉落兼容升级失败", t); + } + } + + @Inject(method = "clearContent", at = @At("TAIL")) + private void eap$compatClear(CallbackInfo ci) { + try { + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + this.eap$compatUpgrades.clear(); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 清理兼容升级失败", t); + } + } + + @Inject(method = "onMainNodeStateChanged", at = @At("TAIL")) + private void eap$compatOnNodeChange(CallbackInfo ci) { + try { + eap$compatLastChannel = -1; + eap$compatHasInitialized = false; + eap$compatDelayedInitTicks = 10; + mainNode.ifPresent((grid, node) -> { + try { grid.getTickManager().wakeDevice(node); } catch (Throwable ignored) {} + }); + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 主节点状态变更处理失败", t); + } + } + + + + @Override + public void eap$updateWirelessLink() { + if (eap$compatLink != null) { + eap$compatLink.updateStatus(); + } + } + + @Unique + public void eap$compatInitializeChannelLink() { + try { + // 客户端早退 + if (host.getBlockEntity() != null && host.getBlockEntity().getLevel() != null && host.getBlockEntity().getLevel().isClientSide) { + return; + } + if (eap$compatHasInitialized) { + return; + } + if (mainNode == null || mainNode.getNode() == null) { + ExtendedAELogger.LOGGER.debug("[样板供应器] 初始化跳过:mainNode 或 Node 不可用"); + return; + } + + long channel = 0L; + boolean found = false; + + IUpgradeInventory upgrades = null; + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + upgrades = this.eap$compatUpgrades; + ExtendedAELogger.LOGGER.debug("[样板供应器] 使用自带升级槽(未安装 appflux): {}", upgrades != null); + } else { + // appflux 应该注入其自身的 IUpgradeableObject 实现 + try { + if ((Object) this instanceof IUpgradeableObject uo) { + upgrades = uo.getUpgrades(); + ExtendedAELogger.LOGGER.debug("[样板供应器] 使用 appflux 提供的升级槽: {}", upgrades != null); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 获取第三方升级槽失败", t); + } + } + + if (upgrades != null) { + for (ItemStack stack : upgrades) { + if (!stack.isEmpty() && stack.getItem() == ModItems.CHANNEL_CARD.get()) { + channel = ChannelCardItem.getChannel(stack); + found = true; + ExtendedAELogger.LOGGER.debug("[样板供应器] 检测到频道卡,频道={} ", channel); + break; + } + } + } + + if (!found) { + ExtendedAELogger.LOGGER.debug("[样板供应器] 未发现频道卡 -> 断开无线"); + if (eap$compatLink != null) { + eap$compatLink.setFrequency(0L); + eap$compatLink.updateStatus(); + } + eap$compatHasInitialized = true; + try { host.saveChanges(); } catch (Throwable ignored) {} + return; + } + + if (eap$compatLink == null) { + var endpoint = new GenericNodeEndpointImpl(() -> host.getBlockEntity(), () -> this.mainNode.getNode()); + eap$compatLink = new WirelessSlaveLink(endpoint); + } + + eap$compatLink.setFrequency(channel); + eap$compatLink.updateStatus(); + ExtendedAELogger.LOGGER.debug("[样板供应器] 设置频道={} 连接状态={}", channel, eap$compatLink.isConnected()); + try { host.saveChanges(); } catch (Throwable ignored) {} + mainNode.ifPresent((grid, node) -> { + try { grid.getTickManager().wakeDevice(node); } catch (Throwable ignored) {} + }); + + if (eap$compatLink.isConnected()) { + eap$compatHasInitialized = true; + } else { + eap$compatHasInitialized = false; + eap$compatDelayedInitTicks = Math.max(eap$compatDelayedInitTicks, 5); + mainNode.ifPresent((grid, node) -> { + try { grid.getTickManager().wakeDevice(node); } catch (Throwable ignored) {} + }); + } + } catch (Throwable t) { + ExtendedAELogger.LOGGER.error("[样板供应器] 初始化频道链接失败", t); + } + } + + @Override + public void eap$setClientWirelessState(boolean connected) { + eap$compatClientConnected = connected; + } + + @Override + public boolean eap$isWirelessConnected() { + if (host.getBlockEntity() != null && host.getBlockEntity().getLevel() != null && host.getBlockEntity().getLevel().isClientSide) { + return eap$compatClientConnected; + } else { + return eap$compatLink != null && eap$compatLink.isConnected(); + } + } + + @Override + public boolean eap$hasTickInitialized() { + return eap$compatHasInitialized; + } + + @Override + public void eap$setTickInitialized(boolean initialized) { + eap$compatHasInitialized = initialized; + } + + // CompatUpgradeProvider 实现:仅在未安装 appflux 时由我们提供升级槽 + @Override + public IUpgradeInventory eap$getCompatUpgrades() { + return this.eap$compatUpgrades != null ? this.eap$compatUpgrades : UpgradeInventories.empty(); + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/helpers/patternprovider/PatternProviderLogicTickerMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/helpers/patternprovider/PatternProviderLogicTickerMixin.java new file mode 100644 index 0000000..ab20ac4 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/helpers/patternprovider/PatternProviderLogicTickerMixin.java @@ -0,0 +1,42 @@ +package com.extendedae_plus.mixin.ae2.helpers.patternprovider; + +import appeng.helpers.patternprovider.PatternProviderLogic; +import com.extendedae_plus.bridge.InterfaceWirelessLinkBridge; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +/** + * 注入到 PatternProviderLogic.Ticker 的每tick回调,驱动无线链接状态更新与延迟初始化。 + */ +@Mixin(targets = "appeng.helpers.patternprovider.PatternProviderLogic$Ticker", remap = false) +public abstract class PatternProviderLogicTickerMixin { + + // 访问内部类的外部引用字段(javac 生成名 this$0) + @Shadow(remap = false) + @Final + private PatternProviderLogic this$0; + + @Inject(method = "tickingRequest", at = @At("HEAD")) + private void eap$tickHead(appeng.api.networking.IGridNode node, int ticksSinceLastCall, + CallbackInfoReturnable cir) { + // 仅在服务端处理延迟初始化 + if (node != null && node.getLevel() != null && node.getLevel().isClientSide) { + return; + } + if (this$0 instanceof InterfaceWirelessLinkBridge bridge) { + bridge.eap$handleDelayedInit(); + } + } + + @Inject(method = "tickingRequest", at = @At("TAIL")) + private void eap$tickTail(appeng.api.networking.IGridNode node, int ticksSinceLastCall, + CallbackInfoReturnable cir) { + if (this$0 instanceof InterfaceWirelessLinkBridge bridge) { + bridge.eap$updateWirelessLink(); + } + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/menu/PatternProviderMenuUpgradesMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/menu/PatternProviderMenuUpgradesMixin.java new file mode 100644 index 0000000..e2c947e --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/menu/PatternProviderMenuUpgradesMixin.java @@ -0,0 +1,54 @@ +package com.extendedae_plus.mixin.ae2.menu; + +import appeng.helpers.patternprovider.PatternProviderLogic; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import appeng.menu.AEBaseMenu; +import appeng.menu.ToolboxMenu; +import appeng.menu.implementations.PatternProviderMenu; +import com.extendedae_plus.bridge.IUpgradableMenu; +import com.extendedae_plus.compat.UpgradeSlotCompat; +import com.extendedae_plus.bridge.CompatUpgradeProvider; +import appeng.api.upgrades.IUpgradeableObject; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.inventory.MenuType; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import com.extendedae_plus.util.ExtendedAELogger; + +@Mixin(value = PatternProviderMenu.class, priority = 2000, remap = false) +public abstract class PatternProviderMenuUpgradesMixin extends AEBaseMenu implements IUpgradableMenu { + @Final + @Shadow protected PatternProviderLogic logic; + + @Unique + private ToolboxMenu eap$toolbox; + + @Inject(method = "(Lnet/minecraft/world/inventory/MenuType;ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/patternprovider/PatternProviderLogicHost;)V", + at = @At("TAIL")) + private void eap$initUpgrades(MenuType menuType, int id, Inventory playerInventory, PatternProviderLogicHost host, CallbackInfo ci) { + this.eap$toolbox = new ToolboxMenu(this); + if (UpgradeSlotCompat.shouldEnableUpgradeSlots()) { + // 未安装 appflux:使用我们提供的升级槽 + ExtendedAELogger.LOGGER.debug("[样板供应器][菜单] 注入升级槽: 使用自带 compat 槽"); + this.setupUpgrades(((CompatUpgradeProvider) this.logic).eap$getCompatUpgrades()); + } else { + // 安装 appflux:使用 appflux 注入到 PatternProviderLogic 的升级槽 + ExtendedAELogger.LOGGER.debug("[样板供应器][菜单] 注入升级槽: 使用 appflux 槽"); + this.setupUpgrades(((IUpgradeableObject) this.logic).getUpgrades()); + } + } + + @Override + public ToolboxMenu getToolbox() { + return this.eap$toolbox; + } + + public PatternProviderMenuUpgradesMixin(MenuType menuType, int id, Inventory playerInventory, Object host) { + super(menuType, id, playerInventory, host); + } +} diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index b562441..9fc4e10 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -25,10 +25,13 @@ "ae2.autopattern.CraftingTreeNodeMixin", "ae2.autopattern.CraftingTreeProcessMixin", "ae2.autopattern.PatternProviderLogicContainsRedirectMixin", + "ae2.compat.PatternProviderLogicCompatMixin", "ae2.helpers.InterfaceLogicChannelCardMixin", "ae2.helpers.InterfaceLogicTickerMixin", "ae2.helpers.PatternProviderLogicAdvancedMixin", "ae2.helpers.PatternProviderLogicDoublingMixin", + "ae2.helpers.patternprovider.PatternProviderLogicTickerMixin", + "ae2.menu.PatternProviderMenuUpgradesMixin", "ae2.parts.automation.IOBusPartChannelCardMixin", "ae2.parts.storagebus.StorageBusPartChannelCardMixin", "ae2.menu.ContainerPatternEncodingTermMenuMixin",