From da04b67ef0cec231c5a64f60d25efcc94ec45b64 Mon Sep 17 00:00:00 2001 From: 1820982382 Date: Mon, 17 Aug 2026 15:15:33 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=85=A8=E6=B5=81=E7=A8=8B=E5=AE=8C=E6=88=90]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/container/EssentiaFillHelper.java | 216 ++++++++++++++++++ .../container/EssentiaTerminalMenu.java | 43 ++++ .../common/container/GuiEssentiaTerminal.java | 118 ++++++++++ .../container/ThEEssentiaMenuAccess.java | 22 ++ .../common/container/slot/ReadOnlySlot.java | 58 +++++ 5 files changed, 457 insertions(+) create mode 100644 src/main/java/thaumicenergistics/common/container/EssentiaFillHelper.java create mode 100644 src/main/java/thaumicenergistics/common/container/EssentiaTerminalMenu.java create mode 100644 src/main/java/thaumicenergistics/common/container/GuiEssentiaTerminal.java create mode 100644 src/main/java/thaumicenergistics/common/container/ThEEssentiaMenuAccess.java create mode 100644 src/main/java/thaumicenergistics/common/container/slot/ReadOnlySlot.java diff --git a/src/main/java/thaumicenergistics/common/container/EssentiaFillHelper.java b/src/main/java/thaumicenergistics/common/container/EssentiaFillHelper.java new file mode 100644 index 0000000..b0384c0 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/container/EssentiaFillHelper.java @@ -0,0 +1,216 @@ +package thaumicenergistics.common.container; + +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import appeng.api.config.Actionable; +import appeng.api.networking.energy.IEnergySource; +import appeng.api.networking.security.IActionSource; +import appeng.api.stacks.AEKey; +import appeng.api.storage.MEStorage; +import appeng.api.storage.StorageHelper; + +import thaumcraft.api.aspects.Aspect; +import thaumcraft.api.aspects.AspectList; +import thaumcraft.api.aspects.IEssentiaContainerItem; +import thaumcraft.common.items.JarBlockItem; +import thaumicenergistics.common.integration.appeng.AEssentiaKey; +import thaumicenergistics.common.integration.tc.TCReflection; + +/** + * 源质容器 ↔ ME 网络通用存取逻辑(服务端)。 + *

+ * 与具体菜单解耦:调用方(源质终端覆写、或 ME 终端 mixin)只需提供 + * storage / energySource / actionSource / player / 物品栈,本类完成 + * 罐子与安瓿的装罐、存入网络操作。 + *

+ */ +public final class EssentiaFillHelper { + + private EssentiaFillHelper() {} + + /** 玻璃安瓿固定容量(TC 原版:8 源质/支)。 */ + public static final int PHIAL_CAPACITY = 8; + /** 罐子容量(TC 原版:64 源质/罐)。 */ + public static final int JAR_CAPACITY = 64; + + /** + * 右键网络源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。 + * + * @return true = 处理成功(装罐/装安瓿),false = 无法处理 + */ + public static boolean fillContainerFromGrid(MEStorage storage, IEnergySource energySource, + IActionSource actionSource, Player player, + ItemStack carried, ResourceLocation aspectId) { + if (carried.isEmpty() || aspectId == null) { + return false; + } + AEKey key = AEssentiaKey.of(aspectId); + if (key == null) { + return false; + } + long amountInGrid = storage.getAvailableStacks().get(key); + if (amountInGrid <= 0) { + return false; + } + + // 玻璃安瓿:消耗空安瓿 → 得到装满安瓿(固定 8 源质) + if (carried.getItem() == BuiltInRegistries.ITEM.get( + ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial"))) { + if (amountInGrid < PHIAL_CAPACITY) { + return false; + } + long extracted = storage.extract(key, PHIAL_CAPACITY, Actionable.MODULATE, actionSource); + if (extracted < PHIAL_CAPACITY) { + if (extracted > 0) { + storage.insert(key, extracted, Actionable.MODULATE, actionSource); + } + return false; + } + carried.shrink(1); + // 得到装满安瓿并放入背包(装好后自动放入背包) + ItemStack filled = filledPhialStack(aspectId.getPath()); + if (!player.getInventory().add(filled)) { + player.drop(filled, false); + } + return true; + } + + // 罐子(IEssentiaContainerItem):每次只装 1 个空罐(手堆叠 -1,背包得 1 满罐) + if (!(carried.getItem() instanceof IEssentiaContainerItem container)) { + return false; + } + AspectList existing = container.getAspects(carried); + if (existing != null && !existing.isEmpty()) { + return false; // 罐子非空,不能装 + } + // 部分取出:网络有多少装多少(最多 JAR_CAPACITY 64);不足 64 也能装,不再整组拒绝 + long toExtract = Math.min(amountInGrid, JAR_CAPACITY); + long extracted = storage.extract(key, toExtract, Actionable.MODULATE, actionSource); + if (extracted <= 0) { + return false; + } + Aspect aspect = Aspect.getAspect(aspectId.getPath()); + if (aspect == null) { + storage.insert(key, extracted, Actionable.MODULATE, actionSource); + return false; + } + // 生成 1 个满罐:必须先 copy(在 shrink 之前),因为 count==1 的堆叠 shrink 后 + // 会变成空堆叠,ItemStack.copy() 对空堆叠直接返回全局 EMPTY 单例,setCount(1) + // 会污染该常量产生"空气幽灵堆叠",罐子因此消失。copyWithCount(1) 生成独立副本, + // setAspects 只影响这一个,不影响原堆叠。 + ItemStack filledJar = carried.copyWithCount(1); + // 手堆叠 -1 + carried.shrink(1); + AspectList newAspects = new AspectList(); + newAspects.add(aspect, (int) extracted); + container.setAspects(filledJar, newAspects); + if (!player.getInventory().add(filledJar)) { + player.drop(filledJar, false); + } + return true; + } + + /** + * 把源质容器物品中的源质注入 ME 原生存储,返回处理后的物品栈。 + * 堆叠中每个容器的源质相同(NBT 共享),按"单个量 × 堆叠数"一次注入; + * 全部注入成功才消耗容器,否则保留原堆叠,避免源质凭空消失。 + *

+ * 罐子抽干后留空罐(可重复使用);安瓿抽干后返回空 glass_phial(不消失)。 + * + * @return null = 非源质容器(未处理,调用方应走原逻辑); + * 原 stack = 未注入(网络满/能量不足)或部分处理保留; + * 空罐 = 罐子全部抽干(可重复使用); + * glass_phial 空安瓿 = 安瓿全部抽干(返回空安瓿,不再消耗掉) + */ + @Nullable + public static ItemStack extractContainerContent(MEStorage storage, IEnergySource energySource, + IActionSource actionSource, ItemStack stack) { + if (!(stack.getItem() instanceof IEssentiaContainerItem container)) { + return null; + } + int count = stack.getCount(); + if (count <= 0) { + return stack; + } + AspectList aspects = container.getAspects(stack); + if (aspects == null || aspects.isEmpty()) { + return stack; + } + + // 阶段1:预检(SIMULATE 无副作用)——网络必须能完整容纳全部源质才继续; + // 任一项放不下则整个堆叠原样保留,避免"部分注入后罐子被消耗、剩余源质凭空消失"。 + Aspect[] aspectArr = aspects.getAspects(); + ResourceLocation[] ids = new ResourceLocation[aspectArr.length]; + long[] totals = new long[aspectArr.length]; + int n = 0; + for (Aspect aspect : aspectArr) { + if (aspect == null) { + continue; + } + int perItem = aspects.getAmount(aspect); + if (perItem <= 0) { + continue; + } + ResourceLocation aspectId = TCReflection.getAspectId(aspect); + if (aspectId == null) { + continue; + } + long total = (long) perItem * count; + if (storage.insert(AEssentiaKey.of(aspectId), total, + Actionable.SIMULATE, actionSource) < total) { + return stack; // 网络容量不足:完全不注入,罐子/安瓿原样保留 + } + ids[n] = aspectId; + totals[n] = total; + n++; + } + if (n == 0) { + return stack; + } + + // 阶段2:执行注入(预检已保证容量;若能量不足导致未完全注入,则回滚本次及此前 + // 已注入的量并保留罐子,保证网络数量与罐子状态一致——不丢源质也不复制源质) + for (int i = 0; i < n; i++) { + long injected = StorageHelper.poweredInsert( + energySource, storage, AEssentiaKey.of(ids[i]), totals[i], actionSource); + if (injected < totals[i]) { + if (injected > 0) { + storage.insert(AEssentiaKey.of(ids[i]), injected, + Actionable.MODULATE, actionSource); + } + for (int j = 0; j < i; j++) { + storage.insert(AEssentiaKey.of(ids[j]), totals[j], + Actionable.MODULATE, actionSource); + } + return stack; + } + } + + // 全部注入成功则消耗整个堆叠(罐子留空罐、安瓿返回空 glass_phial) + if (stack.getItem() instanceof JarBlockItem) { + return new ItemStack(stack.getItem()); + } + // 安瓿:返回空安瓿(glass_phial),不直接消失 + ItemStack emptyPhial = new ItemStack(BuiltInRegistries.ITEM.get( + ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")), count); + return emptyPhial; + } + + /** 生成装满的安瓿(phial_of_essentia_<tag>),与 TC JarBlock.filledPhialStack 一致。 */ + private static ItemStack filledPhialStack(String aspectTag) { + ResourceLocation id = ResourceLocation.fromNamespaceAndPath( + "thaumcraft", "phial_of_essentia_" + aspectTag); + net.minecraft.world.item.Item item = BuiltInRegistries.ITEM.getOptional(id) + .orElse(BuiltInRegistries.ITEM.get( + ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial"))); + return new ItemStack(item); + } +} diff --git a/src/main/java/thaumicenergistics/common/container/EssentiaTerminalMenu.java b/src/main/java/thaumicenergistics/common/container/EssentiaTerminalMenu.java new file mode 100644 index 0000000..0b51cb7 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/container/EssentiaTerminalMenu.java @@ -0,0 +1,43 @@ +package thaumicenergistics.common.container; + +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.MenuType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceLocation; +import appeng.api.storage.ITerminalHost; +import appeng.menu.me.common.MEStorageMenu; + +/** + * 源质终端的菜单(有线端与无线端共用)。 + *

+ * 继承 AE2 原版 {@link MEStorageMenu} 的全部普通物品行为(左键/Shift+点击/拖拽均按普通物品处理)。 + * 源质的存入与取出统一由右键交互承担: + *

+ */ +public class EssentiaTerminalMenu extends MEStorageMenu { + + public EssentiaTerminalMenu(MenuType menuType, int id, Inventory ip, ITerminalHost host) { + super(menuType, id, ip, host); + } + + /** + * 右键终端源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。 + * 具体逻辑见 {@link EssentiaFillHelper#fillContainerFromGrid}。 + * + * @param aspectId 源质 aspect 的 ResourceLocation + * @return true = 处理成功(装罐/装安瓿),false = 无法处理 + */ + public boolean fillContainerFromGrid(ResourceLocation aspectId) { + if (isClientSide()) { + return false; + } + var player = getPlayerInventory().player; + var carried = getCarried(); + return EssentiaFillHelper.fillContainerFromGrid( + this.storage, this.energySource, getActionSource(), player, carried, aspectId); + } +} diff --git a/src/main/java/thaumicenergistics/common/container/GuiEssentiaTerminal.java b/src/main/java/thaumicenergistics/common/container/GuiEssentiaTerminal.java new file mode 100644 index 0000000..8410361 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/container/GuiEssentiaTerminal.java @@ -0,0 +1,118 @@ +package thaumicenergistics.common.container; + +import net.minecraft.network.chat.Component; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.ItemStack; + +import org.jetbrains.annotations.Nullable; + +import thaumcraft.api.aspects.AspectList; +import thaumcraft.api.aspects.IEssentiaContainerItem; + +import appeng.client.gui.me.common.MEStorageScreen; +import appeng.client.gui.style.ScreenStyle; + +import net.neoforged.neoforge.network.PacketDistributor; +import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket; +import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket; + +/** + * 源质终端 GUI:在 AE2 通用终端基础上扩展右键交互。 + * + */ +public class GuiEssentiaTerminal extends MEStorageScreen { + + public GuiEssentiaTerminal(EssentiaTerminalMenu menu, Inventory playerInventory, + Component title, ScreenStyle style) { + super(menu, playerInventory, title, style); + } + + @Override + public boolean mouseClicked(double xCoord, double yCoord, int btn) { + if (btn == 1) { + var carried = this.menu.getCarried(); + boolean shiftDown = net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.hasShiftDown(); + // 源质物品来源:优先菜单 carried;为空时回退玩家主手物品 + ItemStack sourceStack = !carried.isEmpty() ? carried + : this.menu.getPlayerInventory().player.getMainHandItem(); + boolean isContainer = isSourceContainer(sourceStack); + boolean containerHasEssentia = isContainer && getContainerEssentia(sourceStack) != null; + + // Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格) + if (shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) { + var slotStack = this.hoveredSlot.getItem(); + if (slotStack.getItem() instanceof IEssentiaContainerItem) { + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot( + this.menu.containerId, this.hoveredSlot.getSlotIndex(), slotStack)); + return true; + } + // 非源质容器 → 放行(AE2 原版 Shift+右键快速移动) + return super.mouseClicked(xCoord, yCoord, btn); + } + + // 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互) + if (!shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) { + return super.mouseClicked(xCoord, yCoord, btn); + } + + // Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当普通物品放入网络 + if (shiftDown && isContainer) { + if (containerHasEssentia) { + this.menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY); + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack( + this.menu.containerId, sourceStack)); + } + return true; + } + + // 普通右键网络区域:手持满容器 → 存入源质 + if (containerHasEssentia) { + this.menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY); + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack( + this.menu.containerId, sourceStack)); + return true; + } + + // 手持空容器:右键源质条目 → 取出装罐;右键网络其他 → 拦截(防被放入网络) + if (isContainer) { + if (this.hoveredSlot instanceof appeng.client.gui.me.common.RepoSlot repoSlot) { + var entry = repoSlot.getEntry(); + if (entry != null && entry.getWhat() instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey key) { + PacketDistributor.sendToServer(EssentiaTerminalFillC2SPacket.create( + this.menu.containerId, key.getId(), sourceStack)); + return true; + } + } + return true; + } + } + return super.mouseClicked(xCoord, yCoord, btn); + } + + /** 源质容器:实现 IEssentiaContainerItem 的物品,或空玻璃安瓿(glass_phial 不实现该接口但可装罐)。 */ + private static boolean isSourceContainer(ItemStack stack) { + if (stack.getItem() instanceof IEssentiaContainerItem) { + return true; + } + return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get( + net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial"))); + } + + /** 若手持物品是源质容器且非空,返回其 AspectList;否则返回 null。 */ + @Nullable + private static AspectList getContainerEssentia(ItemStack stack) { + if (stack.getItem() instanceof IEssentiaContainerItem container) { + AspectList aspects = container.getAspects(stack); + if (aspects != null && !aspects.isEmpty()) { + return aspects; + } + } + return null; + } +} diff --git a/src/main/java/thaumicenergistics/common/container/ThEEssentiaMenuAccess.java b/src/main/java/thaumicenergistics/common/container/ThEEssentiaMenuAccess.java new file mode 100644 index 0000000..e9cfc89 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/container/ThEEssentiaMenuAccess.java @@ -0,0 +1,22 @@ +package thaumicenergistics.common.container; + +import appeng.api.networking.energy.IEnergySource; +import appeng.api.storage.MEStorage; + +/** + * 由 {@code MixinMEStorageMenuAccessors} 注入到 {@code MEStorageMenu} 的访问器接口。 + *

+ * 编译期接口存在、运行期目标类实现,调用方(如网络包处理)通过 + * {@code instanceof ThEEssentiaMenuAccess} 安全访问 ME 存储与能量源。 + *

+ * 注意:必须放在非 mixin 包(mixin 包 {@code thaumicenergistics.mixins.*} 的类 + * 禁止被普通类直接引用)。 + */ +public interface ThEEssentiaMenuAccess { + + /** ME 存储(对应 MEStorageMenu.storage)。 */ + MEStorage thE_getStorage(); + + /** 能量源(对应 MEStorageMenu.energySource)。 */ + IEnergySource thE_getEnergySource(); +} diff --git a/src/main/java/thaumicenergistics/common/container/slot/ReadOnlySlot.java b/src/main/java/thaumicenergistics/common/container/slot/ReadOnlySlot.java new file mode 100644 index 0000000..b802867 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/container/slot/ReadOnlySlot.java @@ -0,0 +1,58 @@ +package thaumicenergistics.common.container.slot; + +import net.minecraft.world.Container; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.items.IItemHandler; + +/** + * 只读展示槽:显示 IItemHandler 中真实物品,但禁止玩家/整理 mod 交互。 + * 与 AE2 ClientReadOnlySlot 等效(保留 inventory 用于渲染,mayPlace/mayPickup/set/remove 空操作)。 + * 用于玩家不交互的纯展示槽(装配器的预存配方槽、目标产物槽)。 + */ +public class ReadOnlySlot extends Slot { + + /** 假 inventory 占位:阻止整理 mod 通过 slot 的 inventory/index 交互。 */ + private static final Container EMPTY_INVENTORY = new SimpleContainer(0); + + private final IItemHandler handler; + private final int handlerIndex; + + public ReadOnlySlot(IItemHandler handler, int index, int xPosition, int yPosition) { + super(EMPTY_INVENTORY, 0, xPosition, yPosition); + this.handler = handler; + this.handlerIndex = index; + } + + @Override + public ItemStack getItem() { + // 读取真实 inventory 用于渲染显示;handlerIndex 是 handler 内索引(Slot.index 是菜单槽序号,勿用) + return handler.getStackInSlot(handlerIndex); + } + + @Override + public boolean mayPlace(ItemStack stack) { + return false; + } + + @Override + public final void set(ItemStack stack) { + } + + @Override + public int getMaxStackSize() { + return 0; + } + + @Override + public ItemStack remove(int amount) { + return ItemStack.EMPTY; + } + + @Override + public boolean mayPickup(Player player) { + return false; + } +}