diff --git a/src/main/java/thaumicenergistics/common/blocks/ThEWrenchHandler.java b/src/main/java/thaumicenergistics/common/blocks/ThEWrenchHandler.java new file mode 100644 index 0000000..077a784 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/blocks/ThEWrenchHandler.java @@ -0,0 +1,67 @@ +package thaumicenergistics.common.blocks; + +import net.minecraft.core.BlockPos; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.neoforged.bus.api.SubscribeEvent; +import net.neoforged.neoforge.common.NeoForge; +import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent; +import thaumicenergistics.ThaumicEnergistics; + +/** + * AE2 扳手拆卸处理。AE2 的 WrenchHook 只处理 AEBaseBlockEntity,ThE 方块 BE 不走该路径; + * 这里用与 WrenchHook 同级的 RightClickBlock 事件,在 Block.useItemOn 之前拦截扳手右键, + * 对 ThE 方块执行拆卸(掉落 + 移除)。 + */ +public final class ThEWrenchHandler { + + private ThEWrenchHandler() { + } + + public static void register() { + NeoForge.EVENT_BUS.register(ThEWrenchHandler.class); + } + + @SubscribeEvent + public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) { + if (event.isCanceled() || event.getHand() != InteractionHand.MAIN_HAND) { + return; + } + var player = event.getEntity(); + if (player.isSpectator()) { + return; + } + var stack = player.getItemInHand(event.getHand()); + if (!player.isShiftKeyDown() || !stack.is(appeng.datagen.providers.tags.ConventionTags.WRENCH)) { + return; + } + + var level = event.getLevel(); + BlockPos pos = event.getPos(); + BlockState state = level.getBlockState(pos); + // 只处理 ThE 方块(按命名空间判断,兼容未继承 ThETileBase 的工作台等) + if (!ThaumicEnergistics.MODID.equals(state.getBlock().builtInRegistryHolder().key().location().getNamespace())) { + return; + } + BlockEntity be = level.getBlockEntity(pos); + if (be == null) { + return; + } + + if (!level.isClientSide) { + // ThE 方块掉落即自身(挖掘走 getDrops 覆写;扳手拆卸直接给方块物品,避免 loot table 为空) + player.getInventory().placeItemBackInInventory(new ItemStack(be.getBlockState().getBlock())); + level.removeBlock(pos, false); + } else { + level.playSound(player, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 0.7F, 1.0F); + } + + event.setCanceled(true); + event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide)); + } +} diff --git a/src/main/java/thaumicenergistics/common/network/EssentiaTerminalDepositC2SPacket.java b/src/main/java/thaumicenergistics/common/network/EssentiaTerminalDepositC2SPacket.java new file mode 100644 index 0000000..cd9dc97 --- /dev/null +++ b/src/main/java/thaumicenergistics/common/network/EssentiaTerminalDepositC2SPacket.java @@ -0,0 +1,80 @@ +package thaumicenergistics.common.network; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import thaumicenergistics.ThaumicEnergistics; + +/** + * 客户端→服务端:源质容器存入 ME 网络。 + * + */ +public record EssentiaTerminalDepositC2SPacket(int containerId, int slotIndex, + net.minecraft.world.item.ItemStack sourceStack) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(ThaumicEnergistics.id("essentia_terminal_deposit")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, EssentiaTerminalDepositC2SPacket::containerId, + ByteBufCodecs.VAR_INT, EssentiaTerminalDepositC2SPacket::slotIndex, + net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC, EssentiaTerminalDepositC2SPacket::sourceStack, + EssentiaTerminalDepositC2SPacket::new + ); + + /** 手持源质物品存入(客户端可能持有在 carried 或主手)。 */ + public static EssentiaTerminalDepositC2SPacket fromClientStack(int containerId, net.minecraft.world.item.ItemStack stack) { + return new EssentiaTerminalDepositC2SPacket(containerId, -1, stack.copy()); + } + + /** 从玩家背包格存入(Shift+右键背包格)。 */ + public static EssentiaTerminalDepositC2SPacket fromSlot(int containerId, int slotIndex, net.minecraft.world.item.ItemStack stack) { + return new EssentiaTerminalDepositC2SPacket(containerId, slotIndex, stack.copy()); + } + + @Override + public Type type() { + return TYPE; + } + + public void handleOnServer(IPayloadContext ctx) { + ctx.enqueueWork(() -> { + // 任意 ME 终端菜单都支持:源质容器右键 → 源质存入网络 + if (ctx.player().containerMenu.containerId == this.containerId + && ctx.player().containerMenu instanceof appeng.menu.me.common.MEStorageMenu menu + && menu instanceof thaumicenergistics.common.container.ThEEssentiaMenuAccess acc) { + var player = menu.getPlayerInventory().player; + // 源质物品:优先包携带的 sourceStack(客户端真实来源);否则回退菜单 carried/主手 + var source = !this.sourceStack.isEmpty() ? this.sourceStack + : (!menu.getCarried().isEmpty() ? menu.getCarried() + : player.getMainHandItem()); + if (!source.isEmpty()) { + var result = thaumicenergistics.common.container.EssentiaFillHelper.extractContainerContent( + acc.thE_getStorage(), acc.thE_getEnergySource(), + menu.getActionSource(), source); + if (result != null) { + if (this.slotIndex >= 0) { + // Shift+右键背包格:空容器留在原背包格 + player.getInventory().setItem(this.slotIndex, result); + } else { + // 手持:清空 carried,空容器放回玩家背包(不留 carried,防被 AE2 自动放入网络) + menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY); + if (!result.isEmpty()) { + if (!player.getInventory().add(result)) { + player.drop(result, false); + } + } + } + } + } + menu.broadcastChanges(); + } + }); + } +} diff --git a/src/main/java/thaumicenergistics/common/network/EssentiaTerminalFillC2SPacket.java b/src/main/java/thaumicenergistics/common/network/EssentiaTerminalFillC2SPacket.java new file mode 100644 index 0000000..df5082a --- /dev/null +++ b/src/main/java/thaumicenergistics/common/network/EssentiaTerminalFillC2SPacket.java @@ -0,0 +1,60 @@ +package thaumicenergistics.common.network; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.neoforged.neoforge.network.handling.IPayloadContext; +import thaumicenergistics.ThaumicEnergistics; + +/** + * 客户端→服务端:源质终端右键源质条目装罐。 + * 客户端已把 serial 解析为 aspect id(服务端无 client repo,无法解析 serial)。 + * 携带 sourceStack:罐子可能持于主手(未进入 carried),服务端直接用客户端栈, + * 避免只读 menu.getCarried() 为空导致装罐静默失败。 + */ +public record EssentiaTerminalFillC2SPacket(int containerId, ResourceLocation aspectId, + ItemStack sourceStack) implements CustomPacketPayload { + + public static final Type TYPE = + new Type<>(ThaumicEnergistics.id("essentia_terminal_fill")); + + public static final StreamCodec STREAM_CODEC = + StreamCodec.composite( + ByteBufCodecs.VAR_INT, EssentiaTerminalFillC2SPacket::containerId, + ResourceLocation.STREAM_CODEC, EssentiaTerminalFillC2SPacket::aspectId, + ItemStack.OPTIONAL_STREAM_CODEC, EssentiaTerminalFillC2SPacket::sourceStack, + EssentiaTerminalFillC2SPacket::new + ); + + /** 装罐:sourceStack 为客户端解析出的源质容器(优先 carried,为空回退主手)。 */ + public static EssentiaTerminalFillC2SPacket create(int containerId, ResourceLocation aspectId, ItemStack stack) { + return new EssentiaTerminalFillC2SPacket(containerId, aspectId, stack.copy()); + } + + @Override + public Type type() { + return TYPE; + } + + public void handleOnServer(IPayloadContext ctx) { + ctx.enqueueWork(() -> { + // 通用化:任意 ME 终端菜单都支持右键源质条目装罐(源质终端/奥术合成终端/AE2 ME 终端等) + if (ctx.player().containerMenu.containerId == this.containerId + && ctx.player().containerMenu instanceof appeng.menu.me.common.MEStorageMenu menu + && menu instanceof thaumicenergistics.common.container.ThEEssentiaMenuAccess acc) { + var player = menu.getPlayerInventory().player; + // 源质物品:优先包携带的 sourceStack(客户端真实来源);否则回退菜单 carried/主手 + var source = !this.sourceStack.isEmpty() ? this.sourceStack + : (!menu.getCarried().isEmpty() ? menu.getCarried() + : player.getMainHandItem()); + thaumicenergistics.common.container.EssentiaFillHelper.fillContainerFromGrid( + acc.thE_getStorage(), acc.thE_getEnergySource(), + menu.getActionSource(), player, source, this.aspectId); + menu.broadcastChanges(); + } + }); + } +} diff --git a/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageMenuAccessors.java b/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageMenuAccessors.java new file mode 100644 index 0000000..b55bf1f --- /dev/null +++ b/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageMenuAccessors.java @@ -0,0 +1,38 @@ +package thaumicenergistics.mixins.ae2; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import appeng.api.networking.energy.IEnergySource; +import appeng.api.storage.MEStorage; +import appeng.menu.me.common.MEStorageMenu; + +import thaumicenergistics.common.container.ThEEssentiaMenuAccess; + +/** + * 为 {@link MEStorageMenu} 实现 {@link ThEEssentiaMenuAccess} 接口, + * 暴露其 protected 的 storage / energySource 供 ThaumicEnergistics 的 + * {@code EssentiaFillHelper} 访问。 + *

+ * 通过 mixin 向目标类追加方法(mixin 内普通方法会被注入到目标类), + * 方法名前缀 thE_ 避免与 AE2 未来版本可能新增的方法冲突。 + */ +@Mixin(targets = "appeng.menu.me.common.MEStorageMenu", remap = false) +public abstract class MixinMEStorageMenuAccessors implements ThEEssentiaMenuAccess { + + @Shadow(remap = false) + protected MEStorage storage; + + @Shadow(remap = false) + protected IEnergySource energySource; + + @Override + public MEStorage thE_getStorage() { + return this.storage; + } + + @Override + public IEnergySource thE_getEnergySource() { + return this.energySource; + } +} diff --git a/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageScreen.java b/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageScreen.java new file mode 100644 index 0000000..90cec3b --- /dev/null +++ b/src/main/java/thaumicenergistics/mixins/ae2/MixinMEStorageScreen.java @@ -0,0 +1,138 @@ +package thaumicenergistics.mixins.ae2; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +import appeng.client.gui.me.common.MEStorageScreen; +import appeng.client.gui.me.common.RepoSlot; + +import net.neoforged.neoforge.network.PacketDistributor; +import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket; +import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket; + +/** + * 让所有 AE2 ME 终端 GUI(ME 终端、合成终端、无线终端等)都支持源质右键交互: + *

    + *
  • Shift+右键玩家背包格:格内是源质容器 → 源质存入网络(空容器留原格)。
  • + *
  • 右键玩家物品栏/背包槽 → 放行(走 AE2 原版物品交互)。
  • + *
  • 右键网络区域(源质条目或空白):手持装有源质的罐子/安瓿 → 源质存入 ME 网络(空容器回背包)。
  • + *
  • 手持空罐/空安瓿 + 右键源质条目 → 从网络取出源质装罐/装安瓿。
  • + *
  • 左键仍是正常物品操作(不受影响)。
  • + *
+ * 源质终端({@code GuiEssentiaTerminal})已覆写 {@code mouseClicked},子类覆写优先于 mixin; + * 此 mixin 覆盖其余未覆写的 ME 终端 GUI。 + */ +@Mixin(targets = "appeng.client.gui.me.common.MEStorageScreen", remap = false) +public abstract class MixinMEStorageScreen { + + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, remap = false) + private void thE_onMouseClickedEssentia(double xCoord, double yCoord, int btn, + CallbackInfoReturnable cir) { + if (btn != 1) { + return; + } + MEStorageScreen self = (MEStorageScreen) (Object) this; + Slot hovered = self.getSlotUnderMouse(); + boolean shiftDown = net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.hasShiftDown(); + var carried = self.getMenu().getCarried(); + // 源质物品来源:优先菜单 carried(拾取物品);为空时回退到玩家主手物品 + // (GUI 打开时主手物品不自动进入 carried,除非先左键点击拿起) + ItemStack sourceStack = !carried.isEmpty() ? carried + : self.getMenu().getPlayerInventory().player.getMainHandItem(); + boolean isContainer = isSourceContainer(sourceStack); + boolean containerHasEssentia = isContainer && hasContainerEssentia(sourceStack); + + // Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格) + if (shiftDown && hovered != null && self.getMenu().isPlayerSideSlot(hovered)) { + var slotStack = hovered.getItem(); + if (slotStack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem) { + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot( + self.getMenu().containerId, hovered.getSlotIndex(), slotStack)); + cir.setReturnValue(true); + return; + } + // 非源质容器 → 放行(AE2 原版 Shift+右键快速移动) + return; + } + + // 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互) + if (!shiftDown && hovered != null && self.getMenu().isPlayerSideSlot(hovered)) { + return; + } + + // Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当作普通物品放入网络 + if (shiftDown && isContainer) { + if (containerHasEssentia) { + // 立即本地清空 carried/主手,防止 AE2 后续把空容器当普通物品放入网络 + clearSource(self, sourceStack, carried); + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack( + self.getMenu().containerId, sourceStack)); + } + cir.setReturnValue(true); + return; + } + + // 普通右键网络区域:手持满容器 → 存入源质 + if (containerHasEssentia) { + // 立即本地清空 carried/主手,防止 AE2 后续把空容器当普通物品放入网络 + clearSource(self, sourceStack, carried); + PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack( + self.getMenu().containerId, sourceStack)); + cir.setReturnValue(true); + return; + } + + // 手持空容器:右键源质条目 → 取出装罐;右键网络其他 → 拦截(防被放入网络) + if (isContainer) { + if (hovered instanceof RepoSlot repoSlot) { + var entry = repoSlot.getEntry(); + if (entry != null && entry.getWhat() instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey key) { + PacketDistributor.sendToServer(EssentiaTerminalFillC2SPacket.create( + self.getMenu().containerId, key.getId(), sourceStack)); + cir.setReturnValue(true); + return; + } + } + cir.setReturnValue(true); + return; + } + + // 非源质容器 → 放行(AE2 原版右键) + } + + /** 清空 source 所在位置(carried 或主手),防止被 AE2 后续当普通物品处理。 */ + private static void clearSource(MEStorageScreen self, ItemStack source, ItemStack carried) { + if (!carried.isEmpty() && carried == source) { + self.getMenu().setCarried(net.minecraft.world.item.ItemStack.EMPTY); + } else { + self.getMenu().getPlayerInventory().player.getInventory().setItem( + self.getMenu().getPlayerInventory().player.getInventory().selected, + net.minecraft.world.item.ItemStack.EMPTY); + } + } + + /** 源质容器:实现 IEssentiaContainerItem 的物品,或空玻璃安瓿(glass_phial 不实现该接口但可装罐)。 */ + private static boolean isSourceContainer(ItemStack stack) { + if (stack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem) { + return true; + } + return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get( + net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial"))); + } + + /** 若手持物品是源质容器且非空,返回 true。 */ + private static boolean hasContainerEssentia(ItemStack stack) { + if (stack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem container) { + var aspects = container.getAspects(stack); + if (aspects != null && !aspects.isEmpty()) { + return true; + } + } + return false; + } +} diff --git a/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json b/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json new file mode 100644 index 0000000..f12909c --- /dev/null +++ b/src/main/resources/data/minecraft/tags/block/mineable/pickaxe.json @@ -0,0 +1,12 @@ +{ + "replace": false, + "values": [ + "thaumicenergistics:essentia_provider", + "thaumicenergistics:infusion_provider", + "thaumicenergistics:arcane_assembler", + "thaumicenergistics:essentia_vibration_chamber", + "thaumicenergistics:knowledge_inscriber", + "thaumicenergistics:essentia_cell_workbench", + "thaumicenergistics:distillation_encoder" + ] +} diff --git a/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json b/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json new file mode 100644 index 0000000..f12909c --- /dev/null +++ b/src/main/resources/data/minecraft/tags/block/needs_stone_tool.json @@ -0,0 +1,12 @@ +{ + "replace": false, + "values": [ + "thaumicenergistics:essentia_provider", + "thaumicenergistics:infusion_provider", + "thaumicenergistics:arcane_assembler", + "thaumicenergistics:essentia_vibration_chamber", + "thaumicenergistics:knowledge_inscriber", + "thaumicenergistics:essentia_cell_workbench", + "thaumicenergistics:distillation_encoder" + ] +} diff --git a/src/main/resources/pack.png b/src/main/resources/pack.png new file mode 100644 index 0000000..dfa0126 Binary files /dev/null and b/src/main/resources/pack.png differ