[全流程完成]

This commit is contained in:
1820982382
2026-08-17 15:15:04 +08:00
parent 286fc2d2c3
commit 13b981a916
47 changed files with 920 additions and 333 deletions
@@ -1,10 +1,12 @@
package thaumicenergistics.common.blocks;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
@@ -14,10 +16,8 @@ import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.phys.BlockHitResult;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumicenergistics.common.tiles.ThETileBase;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
@@ -43,20 +43,16 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock
@Override
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
/**
* 谐振仓:通配吸力模式,不再需要安瓿瓶切换燃料类型。
* 放行默认交互(若后续添加 GUI 则走 getMenuProvider)。
*/
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (level.isClientSide()) return ItemInteractionResult.SUCCESS;
BlockEntity be = level.getBlockEntity(pos);
if (!(be instanceof TileEssentiaVibrationChamber chamber)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
// 检查手持物品是否为源质安瓿瓶
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
AspectList aspects = container.getAspects(stack);
if (aspects == null || aspects.size() == 0) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
Aspect aspect = aspects.getAspects()[0];
if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) {
chamber.setSuctionType(aspect);
return ItemInteractionResult.SUCCESS;
}
// 通配吸力:无需切换,直接放行
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
@@ -68,4 +64,12 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock
}
super.onRemove(state, level, pos, newState, moved);
}
/**
* 挖掘掉落方块自身(不导出方块实体 NBT)。
*/
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return List.of(new ItemStack(this));
}
}
@@ -1,13 +1,21 @@
package thaumicenergistics.common.blocks;
import java.util.List;
import net.minecraft.core.BlockPos;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.phys.BlockHitResult;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.common.tiles.TileInfusionProvider;
@@ -36,6 +44,24 @@ public abstract class ThEBaseEntityBlock extends Block implements EntityBlock {
return (BlockEntityTicker<T>) ticker;
}
/**
* AE2 扳手拆卸。WrenchHook 只处理 AEBaseBlockEntity,ThE 方块 BE 不走该路径,故在此自行拆卸并掉落。
*/
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
Player player, InteractionHand hand, BlockHitResult hit) {
// 扳手拆卸由 ThEWrenchHandler(RightClickBlock 事件)处理,这里不重复
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
/**
* 挖掘掉落方块自身(不导出方块实体 NBT)。
*/
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return List.of(new ItemStack(this));
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
if (!state.is(newState.getBlock())) {
@@ -13,7 +13,7 @@ import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.IVisDiscountGear;
import thaumcraft.api.IWarpingGear;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.init.ModMenuTypes;
@@ -160,16 +160,12 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
}
});
// ===== 4. 3×7 配方槽(只读展示) =====
// ===== 4. 3×7 配方槽(只读展示,AE2 式防整理) =====
for (int r = 0; r < PATTERN_ROWS; r++)
for (int c = 0; c < PATTERN_COLS; c++) {
int invIdx = TileArcaneAssembler.PATTERN_SLOT_START + r * PATTERN_COLS + c;
addSlot(new SlotItemHandler(machineHandler, invIdx,
PATTERN_X + c * 18, PATTERN_Y + r * 18) {
@Override public boolean mayPlace(ItemStack s) { return false; }
@Override public boolean mayPickup(Player p) { return false; }
@Override public int getMaxStackSize() { return 1; }
});
addSlot(new ReadOnlySlot(machineHandler, invIdx,
PATTERN_X + c * 18, PATTERN_Y + r * 18));
}
// ===== 5. 4张AE2速度卡 =====
@@ -182,11 +178,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
});
}
// ===== 6. 目标产物输出槽(只读展示) =====
addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y) {
@Override public boolean mayPlace(ItemStack s) { return false; }
@Override public boolean mayPickup(Player p) { return false; }
});
// ===== 6. 目标产物输出槽(只读展示,AE2 式防整理) =====
addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y));
// ===== 7. 4个护甲折扣槽 =====
for (int i = 0; i < ARMOR_COUNT; i++) {
@@ -202,9 +195,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
@Override public boolean mayPlace(ItemStack s) {
if (s.getItem() instanceof IVisDiscountGear) return true;
if (s.getItem() instanceof IWarpingGear) return true;
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem
&& eqItem.getEquipmentSlot() == eq) return true;
return s.canEquip(eq, null);
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem)
return eqItem.getEquipmentSlot() == eq;
return false;
}
@Override public void setChanged() {
super.setChanged();
@@ -220,13 +213,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
this(id, inv, resolveTile(inv, buf));
}
/** 临时诊断:确认客户端能否拿到 tile(定位后删除) */
private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) {
var pos = buf.readBlockPos();
var be = inv.player.level().getBlockEntity(pos);
ThaumicEnergistics.LOG.info("[AA-DEBUG] client ctor: pos={}, be={}",
pos, be == null ? "null" : be.getClass().getSimpleName());
return (TileArcaneAssembler) be;
return (TileArcaneAssembler) inv.player.level().getBlockEntity(pos);
}
private ItemStackHandler wrapMachineInventory() {
@@ -33,9 +33,11 @@ import appeng.me.storage.LinkStatusRespectingInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.wands.WandCap;
import net.minecraft.world.item.crafting.Ingredient;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumcraft.common.vis.TCVisHelper;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
@@ -55,6 +57,34 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
*/
private boolean isCraftingResultBeingTaken = false;
/** vis 恢复检测节流:每 10 tick(0.5 秒)检查一次,避免持续遍历配方。 */
private static final int VIS_RECOVERY_CHECK_INTERVAL = 10;
private int visRecoveryTickCounter = 0;
@Override
public void broadcastChanges() {
super.broadcastChanges();
// vis 充能恢复后自动重算结果槽:结果槽空 + 奥术配方有效 + vis 已足够 → 重新显示产物。
// 节流:仅"待恢复"状态每 10 tick 检查一次(findMatchingArcaneRecipe 只在满足时执行)
var player = getPlayerInventory().player;
if (player.level().isClientSide() || !hasValidRecipe || !resultSlot.getItem().isEmpty()) {
return;
}
if (++visRecoveryTickCounter < VIS_RECOVERY_CHECK_INTERVAL) {
return;
}
visRecoveryTickCounter = 0;
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
if (!arcaneResult.isEmpty()) {
AspectList aspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
if (aspects != null && hasEnoughVis(aspects)) {
resultSlot.setRequiredAspects(aspects);
resultSlot.setDisplayedCraftingOutput(arcaneResult);
}
}
}
private final ISegmentedInventory craftingInventoryHost;
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
private final ArcaneCraftingResultSlot resultSlot;
@@ -76,9 +106,9 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus);
this.resultSlot = new ArcaneCraftingResultSlot(getPlayerInventory().player, craftingGridInv);
this.resultSlot.setRefreshCallback(() -> updateCurrentRecipeAndOutput(false));
this.resultSlot.setContainer(this);
this.resultSlot = new ArcaneCraftingResultSlot(
getPlayerInventory().player, this, craftingGridInv, craftingGridInv,
linkStatusInventory, this.getEnergySource(), this.getActionSource());
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
@@ -88,7 +118,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return 1;
}
@Override
public boolean mayPlace(ItemStack stack) {
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
@@ -145,6 +174,26 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
private static final int CENTIVIS = 100;
/**
* 复刻 TC port {@code WandCastingItem.consumptionModifier}:法杖 cap 折扣 − 玩家装备/饰品折扣 − 法杖折扣。
* 仅用于显示(GUI 所需 vis),实际扣费由 consumeVisCost 内部同公式处理。
*/
private float getConsumptionModifier(Aspect aspect, boolean crafting) {
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0);
if (wandStack.isEmpty() || !(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return 1.0f;
}
float modifier = wand.consumptionModifier(wandStack, getPlayerInventory().player, aspect, crafting);
return modifier;
}
/** 折扣后的 vis 成本(GUI 显示用)。 */
private int getDiscountedVisCost(Aspect aspect, int rawCost) {
if (rawCost <= 0 || aspect == null) return 0;
return Math.max(0, Math.round(rawCost * getConsumptionModifier(aspect, true)));
}
private boolean hasEnoughVis(AspectList cost) {
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
if (cost == null || cost.isEmpty()) {
@@ -172,7 +221,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate);
var player = getPlayerInventory().player;
var level = player.level();
@@ -183,27 +231,20 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] findMatchingArcaneRecipe result={}", arcaneResult);
if (!arcaneResult.isEmpty()) {
requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] ARCANE MATCH! aspects={}, result={}", requiredAspects, arcaneResult);
foundArcane = true;
if (hasEnoughVis(requiredAspects)) {
ThaumicEnergistics.LOG.debug("[R] Vis OK → showing result");
resultSlot.setDisplayedCraftingOutput(arcaneResult);
} else {
ThaumicEnergistics.LOG.debug("[R] Vis INSUFFICIENT → result empty, but recipe recognized");
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
}
} else {
ThaumicEnergistics.LOG.debug("[R] No arcane recipe match");
}
}
if (!foundArcane) {
ThaumicEnergistics.LOG.debug("[R] No arcane match, trying vanilla...");
var testItems = new ArrayList<ItemStack>(this.craftingSlots.length);
for (var craftingSlot : this.craftingSlots) {
testItems.add(craftingSlot.getItem().copy());
@@ -212,10 +253,8 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var recipeHolder = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, testInput, level);
if (recipeHolder.isPresent()) {
ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess());
ThaumicEnergistics.LOG.debug("[R] Vanilla match: {}, setting result slot", result);
resultSlot.setDisplayedCraftingOutput(result);
} else {
ThaumicEnergistics.LOG.debug("[R] No vanilla match either, clearing result slot");
// 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态
if (!level.isClientSide()) {
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
@@ -225,9 +264,7 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
if (foundArcane) {
// 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值
ThaumicEnergistics.LOG.debug("[R] foundArcane branch: isClientSide={}", level.isClientSide());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: keeping server-synced values, not overriding");
return;
}
this.hasValidRecipe = true;
@@ -235,32 +272,27 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var ws = wandInv.getStackInSlot(0);
resultSlot.setWandStack(ws);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=true, wandStack={}", ws);
if (requiredAspects != null && requiredAspects.size() > 0) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Aspect, Integer> entry : requiredAspects.aspects.entrySet()) {
if (!sb.isEmpty()) sb.append(",");
sb.append(entry.getKey().getTag()).append("=").append(entry.getValue());
// 存折扣后数值(GUI 显示实际所需 vis,含法杖 + 装备 + 饰品减免)
sb.append(entry.getKey().getTag()).append("=")
.append(getDiscountedVisCost(entry.getKey(), entry.getValue()));
}
this.requiredAspectsData = sb.toString();
ThaumicEnergistics.LOG.debug("[R] aspectsData={}", this.requiredAspectsData);
} else {
this.requiredAspectsData = "";
ThaumicEnergistics.LOG.debug("[R] aspectsData=empty (no aspects)");
}
} else {
ThaumicEnergistics.LOG.debug("[R] else branch: isClientSide={}, levelClass={}",
level.isClientSide(), level.getClass().getSimpleName());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: not overriding synced values (no arcane recipe on client)");
return;
}
this.requiredAspectsData = "";
this.hasValidRecipe = false;
resultSlot.setRequiredAspects(null);
resultSlot.setWandStack(ItemStack.EMPTY);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=false (no arcane recipe)");
}
}
@@ -24,6 +24,7 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.research.TCResearchManager;
import thaumcraft.common.research.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
@@ -98,20 +99,10 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) {
int y = SLOT_ASPECTS_Y + i * 18;
addSlot(new SlotItemHandler(aspectInventory, i, SLOT_ASPECTS_X, y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
addSlot(new ReadOnlySlot(aspectInventory, i, SLOT_ASPECTS_X, y));
}
addSlot(new SlotItemHandler(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
addSlot(new ReadOnlySlot(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y));
if (encoder != null) {
// 源物品槽:ghost 模板(背包拖入不消耗——实际物品还在手里;空手点击清空;JEI 拖拽设置模板)
@@ -1,20 +0,0 @@
package thaumicenergistics.common.container;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumicenergistics.init.ModMenuTypes;
/** 源质元件终端的容器。基于网格的源质视图 + 元件槽。 */
public class ContainerEssentiaCellTerminal extends ThEContainerBase {
final ItemStackHandler cellSlot = new ItemStackHandler(1);
final ItemStackHandler viewSlot = new ItemStackHandler(1);
public ContainerEssentiaCellTerminal(int id, Inventory inv) {
super(ModMenuTypes.ESSENTIA_TERMINAL.get(), id, inv, 2);
addSlot(new SlotItemHandler(cellSlot, 0, 26, 16));
addSlot(new SlotItemHandler(viewSlot, 0, 134, 16));
}
public ContainerEssentiaCellTerminal(int id, Inventory inv, RegistryFriendlyByteBuf buf) { this(id, inv); }
}
@@ -25,6 +25,7 @@ import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
@@ -121,12 +122,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
initPatternSlots();
initCraftingSlots();
addSlot(new SlotItemHandler(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
addSlot(new ReadOnlySlot(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y));
saveStateSlot = addDataSlot(DataSlot.standalone());
saveStateSlot.set(0);
@@ -7,9 +7,10 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ContainerData;
import thaumicenergistics.ThaumicEnergistics;
import org.anti_ad.mc.ipn.api.IPNIgnore;
/**
* 奥术装配器 GUI。
* 奥术装配器 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
*
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
* 第1块 主矩形:(0,0) → 176×197
@@ -20,6 +21,7 @@ import thaumicenergistics.ThaumicEnergistics;
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
*/
@IPNIgnore
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
@@ -11,17 +11,13 @@ import appeng.client.gui.style.ScreenStyle;
import java.util.Map;
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
import org.anti_ad.mc.ipn.api.IPNIgnore;
/** 6 个要素图标位置(左上角坐标) */
private static final int[][] ASPECT_POSITIONS = {
{10, 95}, // 左列 第1行
{10, 112}, // 左列 第2行
{10, 128}, // 左列 第3行
{80, 95}, // 右列 第1行
{80, 112}, // 右列 第2行
{80, 128}, // 右列 第3行
};
/**
* 奥术合成终端 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
*/
@IPNIgnore
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
private static final int ICON_SIZE = 14;
@@ -30,9 +26,6 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
super(menu, playerInventory, title, style);
}
/** 合成结果槽锚点 Y(首次计算缓存,避免每帧遍历 slots)。 */
private int cachedAnchorY = -1;
@Override
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
super.drawFG(guiGraphics, offsetX, offsetY, mouseX, mouseY);
@@ -42,22 +35,17 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
Map<String, Integer> aspectMap = this.menu.getParsedAspects();
if (aspectMap.isEmpty()) return;
// 获取合成结果槽的 Y 位置作为锚点(槽位置不变,首次计算后缓存,避免每帧遍历)
int anchorY;
if (cachedAnchorY < 0) {
for (var slot : this.menu.slots) {
// CRAFTING_RESULT 槽在 style JSON 中 bottom:140
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
cachedAnchorY = slot.y;
break;
}
// 锚点取合成结果槽的 Y:圆环紧贴结果槽左侧,窗口缩放时随槽位一起移动
int anchorY = -1;
for (var slot : this.menu.slots) {
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
anchorY = slot.y;
break;
}
// 若找不到锚点则回退到硬编码值
if (cachedAnchorY < 0) cachedAnchorY = 95;
}
anchorY = cachedAnchorY;
if (anchorY < 0) anchorY = 95;
// 要素圆圈 Y = 锚点 + 偏移(-16/1/17,对应纹理 95/112/128)
// 两列三行:左列 x=10,右列 x=80;行偏移对应三行圆环
int[] aspectOffsets = { -16, 1, 17 };
int index = 0;
@@ -1,15 +0,0 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumicenergistics.ThaumicEnergistics;
public class GuiEssentiaCellTerminal extends AbstractContainerScreen<ContainerEssentiaCellTerminal> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID,"textures/gui/essentia_terminal.png");
public GuiEssentiaCellTerminal(ContainerEssentiaCellTerminal m, Inventory inv, Component t) { super(m,inv,t); imageWidth=176; imageHeight=166; inventoryLabelY=imageHeight-94; }
@Override protected void renderBg(GuiGraphics g, float pt, int mx, int my) { int x=(width-imageWidth)/2,y=(height-imageHeight)/2; g.blit(TEX,x,y,0,0,imageWidth,imageHeight); }
@Override public void render(GuiGraphics g, int mx, int my, float pt) { super.render(g,mx,my,pt); renderTooltip(g,mx,my); }
}
@@ -4,37 +4,44 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import appeng.api.inventories.InternalInventory;
import appeng.menu.slot.AppEngCraftingSlot;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.networking.security.IActionSource;
import appeng.api.storage.MEStorage;
import appeng.helpers.ICraftingGridMenu;
import appeng.helpers.InventoryAction;
import appeng.menu.slot.CraftingTermSlot;
import appeng.util.inv.PlayerInternalInventory;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
/**
* 奥术合成结果槽:在取走合成结果时消耗法杖要素。
* 参考 TC4 1.21.1 ArcaneWorkbenchMenu.onResultTaken + consumeVis。
* 奥术合成结果槽:继承 AE2 CraftingTermSlot 使客户端识别(shift/右键/space 合成自动生效),
* 并保留 vis 消耗检查(mayPickup)与取走时 vis 扣除 + 自动补货(onTake)。
*/
public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
public class ArcaneCraftingResultSlot extends CraftingTermSlot {
/** TC4 内部使用 centivis,1 vis = 100 centivis */
private static final int CENTIVIS = 100;
private AspectList requiredAspects = null;
private ItemStack wandStack = ItemStack.EMPTY;
private Runnable refreshCallback = null;
private ContainerArcaneCraftingTerminal container = null;
private final MEStorage storage;
private final IEnergySource energySrc;
private final IActionSource mySrc;
private final InternalInventory craftingGrid;
public ArcaneCraftingResultSlot(Player player, InternalInventory craftingGridInv) {
super(player, craftingGridInv);
}
public void setContainer(ContainerArcaneCraftingTerminal container) {
public ArcaneCraftingResultSlot(Player player, ContainerArcaneCraftingTerminal container,
InternalInventory craftingGridInv, InternalInventory secondMatrix,
MEStorage storage, IEnergySource energySrc, IActionSource mySrc) {
super(player, mySrc, energySrc, storage, craftingGridInv, secondMatrix, (ICraftingGridMenu) container);
this.container = container;
}
public void setRefreshCallback(Runnable callback) {
this.refreshCallback = callback;
this.storage = storage;
this.energySrc = energySrc;
this.mySrc = mySrc;
this.craftingGrid = craftingGridInv;
}
public void setRequiredAspects(AspectList aspects) {
@@ -47,40 +54,177 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
@Override
public boolean mayPickup(Player player) {
if (requiredAspects == null || requiredAspects.isEmpty()) {
return super.mayPickup(player);
// AE2 原版:结果槽不可 vanilla 点按取走,所有取走走 doClick(含 vis 检查)
return false;
}
@Override
public void doClick(InventoryAction action, Player who) {
// vis 检查:奥术配方需法杖有足够 vis,否则不合成
if (requiredAspects != null && !requiredAspects.isEmpty()) {
if (wandStack.isEmpty()) {
return;
}
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return;
}
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
if (!wand.consumeVisCost(wandStack, who, centivisCost, false, true)) {
return;
}
}
// 奥术配方(非 vanilla CRAFTING)不走 AE2 craftItem(findRecipe 只认 vanilla)
// 走自定义合成:从合成格读配方 → 产物 + 消耗材料 + 从网络补货
if (container != null && container.hasValidRecipe) {
doArcaneCraft(action, who);
return;
}
if (wandStack.isEmpty()) return false;
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return false;
// vanilla 配方:严格 AE2(从网络提取材料填合成格)
super.doClick(action, who);
// 消耗 vis(实际扣除)
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
if (wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand) {
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
wand.consumeVisCost(wandStack, who, centivisCost, true, true);
}
}
}
/** 奥术配方合成:从合成格读 Thaumcraft 奥术配方 → 产物 + 消耗材料 + 从网络补货。 */
private void doArcaneCraft(InventoryAction action, Player who) {
if (this.getItem().isEmpty() || isRemote()) {
return;
}
var grid = craftingGrid;
var level = who.level();
if (level.isClientSide()) return;
// 用 Thaumcraft 奥术配方匹配(取配方对象以支持 retainIngredient 保留原料)
var craftContainer = grid.toContainer();
thaumcraft.api.crafting.IArcaneRecipe arcaneRecipe = thaumcraft.common.lib.crafting.ThaumcraftCraftingManager
.findMatchingArcaneRecipeEntry(craftContainer, who);
if (arcaneRecipe == null) {
return;
}
ItemStack arcaneResult = arcaneRecipe.getCraftingResult(craftContainer);
if (arcaneResult.isEmpty()) {
return;
}
// 保留原料(retainIngredient,如元始珍珠):getRemainingItems 返回非空栈的槽位不消耗。
// 对应 TC 奥术工作台行为——珍珠作为"催化"留在合成格,仅消耗构材与虚空金属锭。
net.minecraft.core.NonNullList<net.minecraft.world.item.ItemStack> remainders =
arcaneRecipe.getRemainingItems(craftContainer);
// shift=整组到背包、普通=1 个到手
int maxTimesToCraft;
InternalInventory target;
if (action == InventoryAction.CRAFT_SHIFT || action == InventoryAction.CRAFT_ALL) {
target = new appeng.util.inv.PlayerInternalInventory(who.getInventory());
maxTimesToCraft = (int) Math.floor((double) arcaneResult.getMaxStackSize() / (double) arcaneResult.getCount());
if (action == InventoryAction.CRAFT_ALL) {
maxTimesToCraft *= net.minecraft.world.entity.player.Inventory.INVENTORY_SIZE;
}
} else {
target = new appeng.util.inv.CarriedItemInventory(getMenu());
maxTimesToCraft = 1;
}
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
int crafted = 0;
for (int x = 0; x < maxTimesToCraft; x++) {
// 每轮 vis 检查 + 实际扣减(consume=true):法杖 vis 不足则停
if (requiredAspects != null && !requiredAspects.isEmpty()) {
if (wandStack.isEmpty()) break;
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) break;
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
boolean ok = wand.consumeVisCost(wandStack, who, centivisCost, true, true);
if (!ok) {
break;
}
}
// 目标满或配方失效则停
if (!target.simulateAdd(arcaneResult).isEmpty()) {
break;
}
// 合成前检查奥术配方仍匹配
ItemStack currentResult = thaumcraft.common.lib.crafting.ThaumcraftCraftingManager
.findMatchingArcaneRecipe(grid.toContainer(), who);
if (currentResult.isEmpty()) {
break;
}
// 记录消耗前合成格(首次记录,后续每轮消耗后补货)
var beforeItems = new java.util.HashMap<Integer, ItemStack>();
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) beforeItems.put(i, s.copy());
}
// 消耗合成格材料(每格 1 个);保留原料槽(remainders 非空,如元始珍珠)不消耗
for (int i = 0; i < grid.size(); i++) {
if (!grid.getStackInSlot(i).isEmpty() && remainders.get(i).isEmpty()) {
grid.extractItem(i, 1, false);
}
}
// 从网络补货(对齐 AE2 终端)
if (storage != null && energySrc != null && mySrc != null) {
for (var entry : beforeItems.entrySet()) {
int slot = entry.getKey();
var beforeItem = entry.getValue();
var currentItem = grid.getStackInSlot(slot);
if (currentItem.getCount() < beforeItem.getCount()) {
long needed = beforeItem.getCount() - currentItem.getCount();
var key = appeng.api.stacks.AEItemKey.of(beforeItem);
if (key != null) {
long extracted = appeng.api.storage.StorageHelper.poweredExtraction(
energySrc, storage, key, needed, mySrc);
if (extracted > 0) {
grid.setItemDirect(slot, beforeItem.copyWithCount(
(int) (currentItem.getCount() + extracted)));
}
}
}
}
}
// 产物入目标
var extra = target.addItems(arcaneResult.copy());
if (!extra.isEmpty()) {
appeng.util.Platform.spawnDrops(who.level(), who.blockPosition(), java.util.List.of(extra));
break;
}
crafted++;
}
return wand.consumeVisCost(wandStack, player, centivisCost, false, true);
// 触发容器刷新(重算结果槽)
container.slotsChanged(grid.toContainer());
}
@Override
public void onTake(Player player, ItemStack stack) {
// 记录消耗前各合成格中的物品类型,用于消耗后从 ME 网络补充
java.util.Map<Integer, ItemStack> beforeItems = new java.util.HashMap<>();
if (container != null) {
var grid = container.getCraftingMatrix();
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) {
beforeItems.put(i, s.copy());
}
}
}
if (container != null) {
container.setCraftingResultBeingTaken(true);
}
try {
// 记录消耗前各合成格物品,取走后从 ME 存储自动补回(AE2 终端行为)
java.util.Map<Integer, ItemStack> beforeItems = new java.util.HashMap<>();
var grid = craftingGrid;
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) beforeItems.put(i, s.copy());
}
super.onTake(player, stack);
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
@@ -93,28 +237,22 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
}
}
// 从 ME 网络自动补充消耗掉的合成材料
if (container != null && !beforeItems.isEmpty()) {
var grid = container.getCraftingMatrix();
var node = container.getGridNode();
if (node != null && node.getGrid() != null) {
var storage = node.getGrid().getStorageService().getInventory();
var energy = container.getEnergySource();
var actionSource = container.getActionSource();
// 自动补货:合成格材料被消耗后从 ME 存储补回
if (!beforeItems.isEmpty()) {
if (storage != null && energySrc != null && mySrc != null) {
for (var entry : beforeItems.entrySet()) {
int slot = entry.getKey();
var beforeItem = entry.getValue();
var currentItem = grid.getStackInSlot(slot);
// 如果该格物品变少了(被消耗了),从网络提取补充
if (currentItem.getCount() < beforeItem.getCount()) {
long needed = beforeItem.getCount() - currentItem.getCount();
var key = appeng.api.stacks.AEItemKey.of(beforeItem);
if (key != null) {
long extracted = appeng.api.storage.StorageHelper.poweredExtraction(
energy, storage, key, needed, actionSource);
energySrc, storage, key, needed, mySrc);
if (extracted > 0) {
var toPlace = beforeItem.copyWithCount((int) (currentItem.getCount() + extracted));
grid.setItemDirect(slot, toPlace);
grid.setItemDirect(slot, beforeItem.copyWithCount(
(int) (currentItem.getCount() + extracted)));
}
}
}
@@ -126,8 +264,6 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
container.setCraftingResultBeingTaken(false);
}
}
if (refreshCallback != null) {
refreshCallback.run();
}
}
}
@@ -128,7 +128,7 @@ public final class RecipeRegistration {
"lBl",
"QPQ",
'Q', ae("quartz_glass"),
'l', new ItemStack(Items.LAPIS_LAZULI, 4),
'l', new ItemStack(Items.LAPIS_LAZULI, 1),
'B', tc("zombie_brain"),
'P', ae("calculation_processor"));
@@ -138,7 +138,7 @@ public final class RecipeRegistration {
"qWq",
"FqA",
'A', tc("air_shard"),
'q', new ItemStack(Items.QUARTZ, 4),
'q', new ItemStack(Items.QUARTZ, 1),
'F', tc("fire_shard"),
'W', ae("certus_quartz_wrench"));
@@ -156,22 +156,23 @@ public final class RecipeRegistration {
ESSENTIA_IMPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_IMPORT_BUS),
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
"JDJ",
// 注意:TC ShapedArcaneRecipe 要求模式为矩形(各行等长),删 J 后必须补空格占位
"JD ",
"IFI",
" ",
'J', tc("warded_jar"),
'J', tc("jar_of_essentia"),
'D', stack(ModItems.DIFFUSION_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'F', tc("essentia_filter"));
ESSENTIA_EXPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_EXPORT_BUS),
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
"JCJ",
"JC ",
"IFI",
" ",
'J', tc("warded_jar"),
'J', tc("jar_of_essentia"),
'C', stack(ModItems.COALESCENCE_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'F', tc("essentia_filter"));
ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS),
@@ -182,7 +183,7 @@ public final class RecipeRegistration {
'D', stack(ModItems.DIFFUSION_CORE),
'C', stack(ModItems.COALESCENCE_CORE),
'F', tc("essentia_filter"),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'G', ae("quartz_glass"));
KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
@@ -190,7 +191,7 @@ public final class RecipeRegistration {
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'I', new ItemStack(Items.IRON_INGOT, 1),
'L', MONITORS,
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
@@ -200,7 +201,7 @@ public final class RecipeRegistration {
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'I', new ItemStack(Items.IRON_INGOT, 1),
'L', MONITORS,
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
@@ -252,7 +253,7 @@ public final class RecipeRegistration {
// 6. 源流接口
VIS_INTERFACE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("VISINTERFACE", stack(ModItems.VIS_INTERFACE),
new AspectList().add(Aspect.AIR, 2).add(Aspect.EARTH, 2).add(Aspect.FIRE, 2).add(Aspect.WATER, 2).add(Aspect.ORDER, 2).add(Aspect.ENTROPY, 2),
tc("balance_shard"),
tc("balanced_shard"),
ae("me_p2p_tunnel"));
// 7. 原质振荡室
@@ -293,27 +294,38 @@ public final class RecipeRegistration {
tc("salis_mundus"));
// 3. 奥术装配器
// 权杖要求:6 个原质要素值全满(满充能)。createFullVisScepter() 生成"满充能"预期栈,
// TC port 的 InfusionRecipe.hasExpectedComponents 会逐键比对 custom_data——
// 预期键必须全部存在于实物且数值相等 → 未满充能的权杖无法匹配。
// 组件数:8 个(1.7.10 原版一致)。实测 9 组件(含世界盐)注魔祭坛无法启动——
// TC 自身所有注魔配方组件数上限就是 8。
ARCANE_ASSEMBLER = ThaumcraftApi.addInfusionCraftingRecipe("ARCANEASSEMBLER", stack(ModBlocks.ARCANE_ASSEMBLER),
5, new AspectList().add(Aspect.CRAFT, 64).add(Aspect.EXCHANGE, 32).add(Aspect.AURA, 16).add(Aspect.MAGIC, 16).add(Aspect.METAL, 8).add(Aspect.CRYSTAL, 8),
ae("molecular_assembler"),
tc("fire_shard"),
tc("entropy_shard"),
tc("order_shard"),
tc("balance_shard"),
tc("balanced_shard"),
tc("water_shard"),
tc("earth_shard"),
tc("air_shard"),
tc("salis_mundus"),
createFullVisScepter());
}
/**
* 生成"满充能"银木神秘权杖作为注魔配方的预期组件。
*
* ⚠ 关键:不要调用 TCVisHelper.setWandParts()!玩家实际合成的权杖(ArcaneSceptreRecipe →
* WandCastingItem.createWandFor)是纯物品,custom_data 只有充能时写入的 tc_vis_* 键,
* 没有 tc_wand_rod/tc_wand_cap。若在预期侧写入这些键,hasExpectedComponents 逐键比对时
* 会因实物缺少而永不匹配——满充能权杖也放不进去。
*
* getWandMaxVis 用 fallbackVariant "silverwood_thaumium_scepter" 解析即可得 15000
* (银木杆 10000 × 权杖 1.5),无需依赖栈上的部件数据。
*/
private static ItemStack createFullVisScepter() {
ItemStack scepter = tc("silverwood_thaumium_scepter");
if (scepter.isEmpty()) return scepter;
TCVisHelper.setWandParts(scepter, "silverwood", "thaumium", true);
int max = TCVisHelper.getWandMaxVis(scepter, "silverwood_thaumium_scepter");
for (Aspect a : Aspect.getPrimalAspects()) {
TCVisHelper.storeVis(scepter, a, max, max);
@@ -7,13 +7,13 @@ import java.util.Map;
import net.minecraft.resources.ResourceLocation;
import appeng.api.config.Actionable;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridServiceProvider;
import appeng.api.networking.security.IActionSource;
import appeng.api.networking.storage.IStorageService;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.api.networking.security.IActionSource;
import thaumicenergistics.api.grid.IEssentiaGrid;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.api.storage.IAspectStack;
@@ -101,11 +101,28 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider
@Override
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
if (amount <= 0) return 0;
if (!simulate) {
long current = monitor.get(aspectId);
monitor.set(aspectId, current + amount);
// 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化)
var nativeInjected = injectNative(aspectId, amount, simulate);
if (nativeInjected < amount) {
// 原生存储空间不足时剩余部分退回内存 monitor(供注魔供应器等内部读取)
long remaining = amount - nativeInjected;
if (!simulate) {
long current = monitor.get(aspectId);
monitor.set(aspectId, current + remaining);
}
return amount - nativeInjected - remaining; // 全部接受(monitor 兜底)
}
return 0;
return 0; // 全部写入原生存储
}
private long injectNative(ResourceLocation aspectId, long amount, boolean simulate) {
var storage = getNativeStorage();
if (storage == null) return 0;
var key = AEssentiaKey.of(aspectId);
if (key == null) return 0;
return StorageHelper.poweredInsert(
grid.getEnergyService(), storage, key, amount,
IActionSource.empty(), simulate ? Actionable.SIMULATE : Actionable.MODULATE);
}
@Override
@@ -34,11 +34,11 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
CompoundTag data = accessor.getServerData();
if (data == null || data.isEmpty()) return;
// 吸力类型
// 当前仓内源质类型(通配吸力模式下显示仓内实际燃料)
if (data.contains(TAG_ASPECT)) {
String aspect = data.getString(TAG_ASPECT);
String aspectKey = "thaumicenergistics.aspect." + aspect;
tooltip.add(Component.translatable("thaumicenergistics.jade.suction_type",
tooltip.add(Component.translatable("thaumicenergistics.jade.current_aspect",
Component.translatable(aspectKey)));
}
@@ -93,8 +93,8 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
data.putDouble(TAG_ENERGY_MAX, chamber.getMaxEnergyStorage());
data.putDouble(TAG_ENERGY_OUTPUT, chamber.getMaxOutputPerTick());
data.putBoolean(TAG_GENERATING, chamber.isActive());
if (chamber.getSuctionType(null) != null) {
data.putString(TAG_ASPECT, chamber.getSuctionType(null).getTag());
if (chamber.getEssentiaType(null) != null) {
data.putString(TAG_ASPECT, chamber.getEssentiaType(null).getTag());
}
}
}
@@ -11,7 +11,6 @@ import net.minecraft.world.phys.BlockHitResult;
import appeng.helpers.WirelessTerminalMenuHost;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.menu.locator.ItemMenuHostLocator;
import appeng.menu.me.common.MEStorageMenu;
/**
* 无线源质终端物品。继承 AE2 的WirelessTerminalItem
@@ -25,7 +24,7 @@ public class WirelessEssentiaTerminalItem extends WirelessTerminalItem {
@Override
public MenuType<?> getMenuType() {
return MEStorageMenu.WIRELESS_TYPE;
return thaumicenergistics.init.ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get();
}
@Override
@@ -50,6 +50,20 @@ public final class ThENetworking {
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
// EssentiaTerminalFillC2SPacket — C2S(客户端→服务端,源质终端右键装罐)
registrar.playToServer(
EssentiaTerminalFillC2SPacket.TYPE,
EssentiaTerminalFillC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// EssentiaTerminalDepositC2SPacket — C2S(客户端→服务端,手持源质容器右键存入网络)
registrar.playToServer(
EssentiaTerminalDepositC2SPacket.TYPE,
EssentiaTerminalDepositC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// ResearchGateErrorPacket — S2C(服务端→客户端,研究未解锁拦截提示,携带研究键)
registrar.playToClient(
ResearchGateErrorPacket.TYPE,
@@ -60,7 +60,6 @@ public class EssentiaExportBusPart extends IOBusPart
private static final double IDLE_POWER = 0.5;
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA;
private static final int TRANSFER_RATE = 8;
private boolean isWorkingFlag = false;
@@ -70,6 +69,14 @@ public class EssentiaExportBusPart extends IOBusPart
private long storedEnergy = 0;
/**
* 单次导出量 = 基础速率 8 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。
*/
private long getExportRate() {
return 8L * Math.max(1, getOperationsPerTick());
}
public EssentiaExportBusPart(IPartItem<?> partItem) {
super(TickRates.ExportBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
this.getMainNode().setIdlePowerUsage(IDLE_POWER);
@@ -122,7 +129,7 @@ public class EssentiaExportBusPart extends IOBusPart
if (key == null) return false;
long extracted = grid.getStorageService().getInventory().extract(
key, TRANSFER_RATE, Actionable.MODULATE, IActionSource.ofMachine(this));
key, getExportRate(), Actionable.MODULATE, IActionSource.ofMachine(this));
if (extracted <= 0) return false;
// 尝试注入到相邻方块:先 IAspectContainer,再回退到 IEssentiaTransport
@@ -62,6 +62,14 @@ public class EssentiaImportBusPart extends IOBusPart
private boolean isWorkingFlag = false;
/**
* 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。
*/
private int getTransferRate() {
return TRANSFER_RATE * Math.max(1, getOperationsPerTick());
}
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
selection -> { },
keyType -> keyType == AEssentiaKeyType.INSTANCE);
@@ -104,7 +112,7 @@ public class EssentiaImportBusPart extends IOBusPart
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE);
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, getTransferRate());
if (extracted == null || extracted.isEmpty()) return false;
long injected = this.injectToNetwork(grid, extracted);
@@ -10,7 +10,6 @@ import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel;
import appeng.api.util.KeyTypeSelection;
import appeng.items.parts.PartModels;
import appeng.menu.me.common.MEStorageMenu;
import appeng.parts.PartModel;
import appeng.parts.reporting.AbstractTerminalPart;
import thaumicenergistics.ThaumicEnergistics;
@@ -54,7 +53,7 @@ public class EssentiaTerminalPart extends AbstractTerminalPart {
@Override
public MenuType<?> getMenuType(Player player) {
return MEStorageMenu.TYPE;
return thaumicenergistics.init.ModMenuTypes.ESSENTIA_TERMINAL.get();
}
@Override
@@ -53,6 +53,9 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
/** 抽取颜色在没有新抽取后保持点亮的时间(ms),超时清除。 */
private static final long TIME_TO_CLEAR = 500L;
/** 懒 tick 周期:固定 30 tick(vis 传输为低频操作,无需实时轮询)。 */
private static final int TICK_RATE = 30;
private static final String NBT_KEY_UID = "uid";
private static final String NBT_KEY_IS_PROVIDER = "isProvider";
@@ -290,7 +293,7 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
@Override
public TickingRequest getTickingRequest(IGridNode node) {
return new TickingRequest(30, 30, false);
return new TickingRequest(TICK_RATE, TICK_RATE, false);
}
@Override
@@ -51,7 +51,6 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.IArcaneRecipe;
import thaumicenergistics.api.grid.IEssentiaGrid;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
@@ -314,7 +313,6 @@ public class TileArcaneAssembler extends ThETileBase
boolean enoughVis = hasEnoughVisForCraft();
if (!enoughVis) {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: vis 不足,等待补充 (storedVis cvis={})", storedVis.visSize());
return TickRateModulation.SAME;
}
@@ -331,7 +329,6 @@ public class TileArcaneAssembler extends ThETileBase
long inserted = storage.getInventory().insert(
outputKey, output.getCount(), Actionable.SIMULATE, actionSource);
if (inserted < output.getCount()) {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 产物无法插入网络(模拟 inserted={}/{}),等待", inserted, output.getCount());
return TickRateModulation.SAME;
}
@@ -339,7 +336,6 @@ public class TileArcaneAssembler extends ThETileBase
storage.getInventory().insert(
outputKey, output.getCount(), Actionable.MODULATE, actionSource);
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 完成!插入产物 {}x{} 到网络", outputKey, output.getCount());
isCrafting = false;
currentRecipe = null;
@@ -359,11 +355,8 @@ public class TileArcaneAssembler extends ThETileBase
double extracted = energy.extractAEPower(need, Actionable.MODULATE, PowerMultiplier.CONFIG);
if (extracted >= need * 0.9) {
craftTickCounter += ticksSinceLast;
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need);
// 节流同步合成进度到客户端(GUI 第7格进度条动画)
maybeMarkVisDirty();
} else {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need);
}
}
@@ -446,19 +439,13 @@ public class TileArcaneAssembler extends ThETileBase
ArcaneAssemblerPattern ap = buildArcanePattern(acp);
if (ap != null) {
result.add(ap);
ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: pattern primaryOutput={}, definition={}",
ap.getPrimaryOutput(), ap.getDefinition());
}
}
ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: kCore valid={}, handler patterns={}, returned={}",
!kCore.isEmpty(), patterns.size(), result.size());
return result;
}
@Override
public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) {
ThaumicEnergistics.LOG.info("[AA] pushPattern(provider) called, pattern={}, isArcanePattern={}",
patternDetails, patternDetails instanceof ArcaneAssemblerPattern);
if (patternDetails instanceof ArcaneAssemblerPattern arcanePattern) {
return beginCrafting(arcanePattern);
}
@@ -490,9 +477,6 @@ public class TileArcaneAssembler extends ThETileBase
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
AspectList aspects = arcanePattern.getAspects();
ItemStack result = arcanePattern.getResultStack();
ThaumicEnergistics.LOG.info("[AA] beginCrafting: isCrafting={}, active={}, aspectsEmpty={}, resultEmpty={}",
isCrafting, mainNode.isActive(),
aspects == null || aspects.isEmpty(), result.isEmpty());
if (isCrafting || !mainNode.isActive()) return false;
if (aspects == null || aspects.isEmpty() || result.isEmpty()) return false;
@@ -504,7 +488,6 @@ public class TileArcaneAssembler extends ThETileBase
this.internalInventory.setItem(TARGET_SLOT, result.copy());
setChanged();
markForUpdate();
ThaumicEnergistics.LOG.info("[AA] beginCrafting: STARTED, result={}, aspects={}", result, aspects.visSize());
return true;
}
@@ -593,7 +576,6 @@ public class TileArcaneAssembler extends ThETileBase
// 守卫 getGrid()!=null:装配器未连 ME 网络(无相邻 cable)时 getGrid() 为 null,
// 此时 requestUpdate 内部 node.getGrid().getCraftingService() 会 NPE,导致 kCore 插入事务回滚 → "无法绑定"。
if (level != null && !level.isClientSide() && mainNode.getGrid() != null) {
ThaumicEnergistics.LOG.info("[AA] kCore slot changed → requestUpdate");
ICraftingProvider.requestUpdate(mainNode);
}
}
@@ -40,12 +40,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private static final int MAX_ESSENTIA = 64;
private static final int BASE_BURN_TICKS = 800;
private static final double BASE_AE_PER_TICK = 20.0;
private static final double BASE_AE_PER_TICK = 200.0;
private static final int IDLE_POWER = 1;
// 能量槽配置:8KFE = 8000 AE,对外最大输出 400 AE/t
public static final double MAX_ENERGY_STORAGE = 8000.0;
public static final double MAX_OUTPUT_PER_TICK = 400.0;
// 能量槽配置:16KFE = 16000 AE,对外最大输出 1000 AE/t
public static final double MAX_ENERGY_STORAGE = 16000.0;
public static final double MAX_OUTPUT_PER_TICK = 2000.0;
// 懒 tick 周期:燃烧中 10 tick、空闲 40 tick(能量生产无需实时响应,低频即可)
private static final int TICK_RATE_BURNING = 10;
private static final int TICK_RATE_IDLE = 40;
protected final IManagedGridNode mainNode;
private final IGridNodeListener<TileEssentiaVibrationChamber> nodeListener = new IGridNodeListener<>() {
@@ -62,7 +66,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private double aePerTick;
private int storedEssentia;
private Aspect currentAspect;
private Aspect suctionAspect = Aspect.ENERGY;
private double storedEnergy;
@@ -97,7 +100,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override
public TickingRequest getTickingRequest(IGridNode node) {
return new TickingRequest(10, 40, false);
return new TickingRequest(TICK_RATE_BURNING, TICK_RATE_IDLE, false);
}
@Override
@@ -177,8 +180,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
int srcAmt = source.getEssentiaAmount(sourceSide);
if (!canOut) continue;
if (srcType == null) continue;
String srcTag = srcType.getTag();
if (!srcTag.equals("ignis") && !srcTag.equals("potentia") && !srcTag.equals("energy")) continue;
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
if (space <= 0) return;
@@ -187,7 +189,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
if (taken > 0) {
storedEssentia += taken;
currentAspect = srcType;
suctionAspect = srcType;
// suctionAspect 固定为 null(通配吸力),不锁定单一类型
setChanged();
return;
}
@@ -240,9 +242,25 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private boolean startBurning() {
if (currentAspect == null) currentAspect = Aspect.FIRE;
boolean isPotentia = currentAspect != null && (currentAspect == Aspect.ENERGY || currentAspect.getTag().equals("potentia") || currentAspect.getTag().equals("energy"));
int burnTime = isPotentia ? (int)(BASE_BURN_TICKS / 1.6f) : BASE_BURN_TICKS / 2;
double power = isPotentia ? BASE_AE_PER_TICK * 1.6 : BASE_AE_PER_TICK;
String tag = currentAspect.getTag();
boolean isPotentia = currentAspect == Aspect.ENERGY || tag.equals("potentia") || tag.equals("energy");
boolean isOther = !tag.equals("ignis") && !isPotentia;
int burnTime;
double power;
if (isPotentia) {
// 能量:1.6 倍功率,燃烧更久
burnTime = (int)(BASE_BURN_TICKS / 1.6f);
power = BASE_AE_PER_TICK * 1.6;
} else if (isOther) {
// 其他所有要素:基础效率的 1/2
burnTime = BASE_BURN_TICKS / 2;
power = BASE_AE_PER_TICK / 2.0;
} else {
// 火:基础效率
burnTime = BASE_BURN_TICKS / 2;
power = BASE_AE_PER_TICK;
}
burnTicksRemaining = burnTime;
totalBurnTicks = burnTime;
@@ -254,17 +272,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override public boolean canInputFrom(Direction side) { return true; }
@Override public boolean canOutputTo(Direction side) { return true; }
@Override public void setSuction(Aspect aspect, int amount) {
this.suctionAspect = aspect;
// 通配吸力:始终吸所有源质类型,忽略外部设置
}
@Override public Aspect getSuctionType(Direction side) {
return suctionAspect;
return null; // null = 通配吸力:管道可输送任意类型源质
}
@Override public int getSuctionAmount(Direction side) { return 128; }
private boolean isFuelAspect(Aspect aspect) {
if (aspect == null) return false;
String tag = aspect.getTag();
return tag.equals("ignis") || tag.equals("potentia") || tag.equals("energy");
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
return aspect != null;
}
@Override
@@ -298,11 +315,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override public boolean renderExtendedTube() { return false; }
public void setSuctionType(Aspect aspect) {
if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) {
suctionAspect = aspect;
setChanged();
markForUpdate();
}
// 通配吸力模式下无需设置类型;保留方法避免编译错误
}
public int getStoredEssentia() { return storedEssentia; }
@@ -338,7 +351,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
storedEnergy = tag.getDouble("StoredEnergy");
if (storedEnergy > MAX_ENERGY_STORAGE) storedEnergy = MAX_ENERGY_STORAGE;
if (tag.contains("AspectTag")) currentAspect = Aspect.getAspect(tag.getString("AspectTag"));
if (tag.contains("SuctionTag")) suctionAspect = Aspect.getAspect(tag.getString("SuctionTag"));
// suctionAspect 固定为 null(通配吸力),不读取 SuctionTag
}
@Override
@@ -352,6 +365,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
tag.putInt("StoredEssentia", storedEssentia);
tag.putDouble("StoredEnergy", storedEnergy);
if (currentAspect != null) tag.putString("AspectTag", currentAspect.getTag());
if (suctionAspect != null) tag.putString("SuctionTag", suctionAspect.getTag());
// suctionAspect 固定为 null(通配吸力),不写入 SuctionTag
}
}