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 网络。
+ *
+ * - 手持(carried/主手)源质容器右键 → 源质存入,空容器放回玩家背包(slotIndex=-1)。
+ * - Shift+右键玩家背包格 → 源质存入,空容器留在原背包格(slotIndex>=0)。
+ *
+ */
+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 extends CustomPacketPayload> 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 extends CustomPacketPayload> 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