[全流程完成]

This commit is contained in:
1820982382
2026-08-17 15:15:33 +08:00
parent 13b981a916
commit da04b67ef0
5 changed files with 457 additions and 0 deletions
@@ -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 网络通用存取逻辑(服务端)。
* <p>
* 与具体菜单解耦:调用方(源质终端覆写、或 ME 终端 mixin)只需提供
* storage / energySource / actionSource / player / 物品栈,本类完成
* 罐子与安瓿的装罐、存入网络操作。
* <ul>
* <li>取出(装罐):网络有源质 → 装入手持空罐(有多少装多少,最多 64)/ 空安瓿(固定 8)。</li>
* <li>存入:手持/点击的罐子或安瓿 → 源质注入网络;罐子留空罐,安瓿返回空 glass_phial(不消失)。</li>
* </ul>
*/
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 共享),按"单个量 × 堆叠数"一次注入;
* 全部注入成功才消耗容器,否则保留原堆叠,避免源质凭空消失。
* <p>
* 罐子抽干后留空罐(可重复使用);安瓿抽干后返回空 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_&lt;tag&gt;),与 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);
}
}
@@ -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;
/**
* 源质终端的菜单(有线端与无线端共用)。
* <p>
* 继承 AE2 原版 {@link MEStorageMenu} 的全部普通物品行为(左键/Shift+点击/拖拽均按普通物品处理)。
* 源质的存入与取出统一由右键交互承担:
* <ul>
* <li>手持装有源质的罐子/安瓿 + 右键任意处 → 源质存入网络({@link EssentiaFillHelper#extractContainerContent})。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出装罐({@link #fillContainerFromGrid})。</li>
* </ul>
*/
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);
}
}
@@ -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 通用终端基础上扩展右键交互。
* <ul>
* <li>Shift+右键玩家背包格:格内是源质容器 → 源质存入网络(空容器留原格)。</li>
* <li>右键玩家物品栏/背包槽 → 放行(AE2 原版物品交互)。</li>
* <li>右键网络区域(源质条目或空白),手持装有源质的罐子/安瓿 → 源质存入 ME 网络(空容器回背包)。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出源质装罐/装安瓿。</li>
* <li>左键仍是正常物品操作(不受影响)。</li>
* </ul>
*/
public class GuiEssentiaTerminal extends MEStorageScreen<EssentiaTerminalMenu> {
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;
}
}
@@ -0,0 +1,22 @@
package thaumicenergistics.common.container;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.storage.MEStorage;
/**
* 由 {@code MixinMEStorageMenuAccessors} 注入到 {@code MEStorageMenu} 的访问器接口。
* <p>
* 编译期接口存在、运行期目标类实现,调用方(如网络包处理)通过
* {@code instanceof ThEEssentiaMenuAccess} 安全访问 ME 存储与能量源。
* <p>
* 注意:必须放在非 mixin 包(mixin 包 {@code thaumicenergistics.mixins.*} 的类
* 禁止被普通类直接引用)。
*/
public interface ThEEssentiaMenuAccess {
/** ME 存储(对应 MEStorageMenu.storage)。 */
MEStorage thE_getStorage();
/** 能量源(对应 MEStorageMenu.energySource)。 */
IEnergySource thE_getEnergySource();
}
@@ -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;
}
}