Compare commits

...
9 Commits
Author SHA1 Message Date
1820982382 b80922e1f1 v2.3.14 2026-09-23 14:50:31 +08:00
1820982382 2e6a8e9590 v2.3.14 2026-09-23 14:49:11 +08:00
1820982382 2ee393b4e6 v2.3.14 2026-09-23 14:47:21 +08:00
1820982382 0efd437209 v2.3.14 2026-09-23 14:39:07 +08:00
1820982382 ad0ead9f53 v2.3.9 2026-08-28 00:43:18 +08:00
1820982382 7a914571d8 v2.3.9 2026-08-28 00:42:45 +08:00
1820982382 14aaf0b1a9 v2.3.9 2026-08-28 00:41:54 +08:00
1820982382 177ef6c551 v2.2.8 2026-08-22 17:07:18 +08:00
1820982382 c83e34c1d6 v2.2.7 2026-08-22 16:59:38 +08:00
77 changed files with 8285 additions and 390 deletions
+4976
View File
File diff suppressed because it is too large Load Diff
+40 -10
View File
@@ -21,7 +21,6 @@ repositories {
}
}
version = mod_version
group = mod_group
var modName = mod_name
@@ -45,9 +44,11 @@ neoForge {
dependencies {
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api"
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17" // 加这行保留!!让AE2内部类可编译
compileOnly files('library/thaumcraft-0.2.2.34-port.1.jar')
compileOnly files('library/thaumcraft-0.2.2.95-port.229.jar') // 必须与整合包运行版本一致(0.2.2.95);.34 缺少 getRemainingItems/findMatchingArcaneRecipeEntry 等 API
compileOnly files('library/jei-1.21.1-neoforge-19.39.0.368.jar')
compileOnly files('library/ae2jeiintegration-1.2.1.jar')
compileOnly files('library/InventoryProfilesNext-neoforge-1.21.1-2.2.5.jar')
compileOnly files('library/extendedae_plus-1.6.1.jar') // eap mixin target(InputEvents.toGenericStack)
compileOnly "maven.modrinth:jade:${jade_version}"
}
@@ -57,7 +58,8 @@ tasks.withType(JavaCompile).configureEach {
it.options.release = 21
}
var modVersion = mod_version
// mods.toml 的 version 也用 git 实时版本(与 jar 版本一致,不依赖写死的 mod_version)
var modVersion = gitVersion()
def loaderRange = project.findProperty('loader_version_range') ?: '[4,)'
def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)'
def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)'
@@ -71,12 +73,40 @@ processResources {
}
}
jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// PCL/启动器显示的 jar 图标(mod 列表图标走 neoforge.mods.toml 的 logoFile)
from('src/main/resources') {
include 'pack.png'
}
}
// 获取git提交总数
def gitCommitCount = providers.exec {
commandLine 'git', 'rev-list', '--count', 'HEAD'
}.standardOutput.asText.map { it.trim() }
// 最终完整版本 = 主版本+构建号
version = "${mod_version}.${gitCommitCount.get()}-alpha"
//// 获取git提交总数
//def gitCommitCount = providers.exec {
// commandLine 'git', 'rev-list', '--count', 'HEAD'
//}.standardOutput.asText.map { it.trim() }
//
//// 最终完整版本 = 主版本+构建号
//version = "${mod_version}.${gitCommitCount.get()}-alpha"
// build.gradle 替换现有 version 逻辑
def gitVersion() {
def describe = providers.exec {
commandLine 'git', 'describe', '--tags', '--always'
}.standardOutput.asText.map { it.trim() }.get()
// 支持 "2.2" / "v2.2.0" / "2.2-3-gabc1234" / "abc1234"(无tag时)
// 版本 = tag 的 X.Y + 提交数:tag 2.2 + 2 提交 → 2.2.2
// find() 只匹配开头,忽略尾部 commit hash(-gabc1234)
def m = describe =~ /^v?(\d+\.\d+)(?:\.\d+)?(?:-(\d+))?/
if (m.find()) {
def xy = m.group(1) // 2.2
def count = m.group(2) ? m.group(2) : '0' // tag 后提交数
return "${xy}.${count}-alpha" // 2.2.2-alpha
}
// 兜底(从未打 tag):0.0.<提交总数>-alpha,不依赖写死的 mod_version
def count = providers.exec {
commandLine 'git', 'rev-list', '--count', 'HEAD'
}.standardOutput.asText.map { it.trim() }.get()
return "0.0.${count}-alpha"
}
version = modVersion
-1
View File
@@ -1,5 +1,4 @@
# Project
mod_version=2.1.1
mod_group=thaumicenergistics
# Mod
@@ -1,5 +1,6 @@
package thaumicenergistics;
//
//AlkSur
import appeng.api.features.GridLinkables;
import appeng.api.features.P2PTunnelAttunement;
import appeng.api.parts.PartModels;
@@ -1,5 +1,7 @@
package thaumicenergistics.common.container;
import java.util.List;
import appeng.core.definitions.AEItems;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
@@ -33,28 +35,28 @@ import thaumicenergistics.init.ModMenuTypes;
*/
public class ContainerArcaneAssembler extends AbstractContainerMenu {
// ===== 坐标常量(1:1 对应1.7.10) =====
// ===== 坐标常量(新版贴图 256×256:格子 16×16、步距 18) =====
private static final int PLAYER_INV_X = 8;
private static final int PLAYER_INV_Y = 113;
private static final int HOTBAR_Y = PLAYER_INV_Y + 58; // 173
private static final int PLAYER_INV_Y = 147;
private static final int HOTBAR_Y = 205;
private static final int PATTERN_X = 26;
private static final int PATTERN_Y = 25;
private static final int PATTERN_Y = 15;
private static final int PATTERN_ROWS = 3;
private static final int PATTERN_COLS = 7;
private static final int KCORE_X = 187;
private static final int KCORE_Y = 8;
private static final int KCORE_X = 175;
private static final int KCORE_Y = 6;
private static final int UPGRADE_X = 187;
private static final int UPGRADE_Y = 26;
private static final int UPGRADE_X = 175;
private static final int UPGRADE_Y = 24;
private static final int UPGRADE_COUNT = 4;
private static final int TARGET_X = 14;
private static final int TARGET_Y = 87;
private static final int TARGET_X = 114;
private static final int TARGET_Y = 97;
private static final int ARMOR_X = 210;
private static final int ARMOR_Y = 26;
private static final int ARMOR_X = 152;
private static final int ARMOR_Y = 73;
private static final int ARMOR_COUNT = 4;
// ===== 槽位划分(quickMoveStack 用) =====
@@ -69,20 +71,28 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
private static final int ARMOR_START = TARGET_IDX + 1; // 63
private static final int ARMOR_END = ARMOR_START + ARMOR_COUNT; // 67
/** 升级卡 handler(独立4格,与 Tile 的 speedUpgrades 字段同步) */
final ItemStackHandler upgradeHandler = new ItemStackHandler(UPGRADE_COUNT) {
@Override protected void onContentsChanged(int slot) {
super.onContentsChanged(slot);
if (assembler != null) {
int count = 0;
for (int i = 0; i < UPGRADE_COUNT; i++)
if (!getStackInSlot(i).isEmpty()) count++;
assembler.setSpeedUpgrades(count);
public final TileArcaneAssembler assembler;
/** 供 GUI 渲染空槽图标(加速卡/护甲)时定位槽位。 */
public Slot getUpgradeSlot(int i) { return slots.get(UPGRADE_START + i); }
public Slot getArmorSlot(int i) { return slots.get(ARMOR_START + i); }
public Slot getKCoreSlot() { return slots.get(KCORE_IDX); }
// ===== 配方预览(客户端,由 AssemblerPreviewSyncPacket 更新) =====
public boolean clientPreviewActive = false;
public ItemStack[] clientPreviewGrid = new ItemStack[9];
public ItemStack clientPreviewResult = ItemStack.EMPTY;
public void applyClientPreview(List<ItemStack> grid, ItemStack result, boolean active) {
this.clientPreviewActive = active;
this.clientPreviewGrid = new ItemStack[9];
if (grid != null) {
for (int i = 0; i < 9 && i < grid.size(); i++) {
this.clientPreviewGrid[i] = grid.get(i) != null ? grid.get(i).copy() : ItemStack.EMPTY;
}
}
};
public final TileArcaneAssembler assembler;
this.clientPreviewResult = result != null ? result.copy() : ItemStack.EMPTY;
}
/** 可视数据同步:服务端读 tile,客户端存同步值(GUI 进度条/源质条不依赖 BE 网络同步,靠容器每 tick 同步) */
public final ContainerData visData;
@@ -168,9 +178,10 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
PATTERN_X + c * 18, PATTERN_Y + r * 18));
}
// ===== 5. 4张AE2速度卡 =====
// ===== 5. 4张AE2速度卡(持久化在 Tile 槽 27~30,关 GUI 不丢失) =====
for (int i = 0; i < UPGRADE_COUNT; i++) {
addSlot(new SlotItemHandler(upgradeHandler, i, UPGRADE_X, UPGRADE_Y + i * 18) {
int invIdx = TileArcaneAssembler.UPGRADE_SLOT_START + i;
addSlot(new SlotItemHandler(machineHandler, invIdx, UPGRADE_X, UPGRADE_Y + i * 18) {
@Override
public boolean mayPlace(ItemStack s) {
return AEItems.SPEED_CARD.is(s);
@@ -178,8 +189,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
});
}
// ===== 6. 目标产物输出槽(只读展示,AE2 式防整理) =====
addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y));
// ===== 6. 目标产物输出槽(只读展示,由 GUI 从网络包数据渲染;槽位移出屏幕避免双渲染) =====
addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, -2000, -2000));
// ===== 7. 4个护甲折扣槽 =====
for (int i = 0; i < ARMOR_COUNT; i++) {
@@ -191,6 +202,13 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
case 2 -> net.minecraft.world.entity.EquipmentSlot.LEGS;
default -> net.minecraft.world.entity.EquipmentSlot.FEET;
};
// 空槽图标:原版头盔/胸甲/护腿/靴子(BLOCK_ATLAS 图集,同 WCWT 手动合成区做法)
final net.minecraft.resources.ResourceLocation armorIcon = switch (i) {
case 0 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_HELMET;
case 1 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_CHESTPLATE;
case 2 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_LEGGINGS;
default -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_BOOTS;
};
addSlot(new SlotItemHandler(machineHandler, invIdx, ARMOR_X, ARMOR_Y + i * 18) {
@Override public boolean mayPlace(ItemStack s) {
if (s.getItem() instanceof IVisDiscountGear) return true;
@@ -199,6 +217,11 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
return eqItem.getEquipmentSlot() == eq;
return false;
}
@Override
public com.mojang.datafixers.util.Pair<net.minecraft.resources.ResourceLocation, net.minecraft.resources.ResourceLocation> getNoItemIcon() {
return com.mojang.datafixers.util.Pair.of(
net.minecraft.world.inventory.InventoryMenu.BLOCK_ATLAS, armorIcon);
}
@Override public void setChanged() {
super.setChanged();
if (assembler != null)
@@ -1,6 +1,7 @@
package thaumicenergistics.common.container;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -22,6 +23,7 @@ import appeng.api.inventories.InternalInventory;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.stacks.AEItemKey;
import appeng.api.storage.ITerminalHost;
import appeng.api.storage.StorageHelper;
import appeng.helpers.ICraftingGridMenu;
import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync;
@@ -30,7 +32,10 @@ import appeng.menu.me.crafting.CraftConfirmMenu;
import appeng.menu.slot.AppEngSlot;
import appeng.menu.slot.CraftingMatrixSlot;
import appeng.me.storage.LinkStatusRespectingInventory;
import appeng.util.inv.PlayerInternalInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.ArcaneTerminalClearC2SPacket;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.wands.WandCap;
@@ -38,7 +43,6 @@ 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;
import thaumicenergistics.init.ModMenuTypes;
@@ -133,15 +137,15 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}, SlotSemantics.STORAGE);
updateCurrentRecipeAndOutput(true);
registerClientAction(ACTION_CLEAR_TO_PLAYER, this::clearToPlayerInventory);
}
// ========== ICraftingGridMenu 实现 ==========
@Override
public InternalInventory getCraftingMatrix() {
var inv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
ThaumicEnergistics.LOG.debug("[ICraftingGrid] getCraftingMatrix called, size={}", inv.size());
return inv;
return this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
}
@Override
@@ -154,21 +158,117 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return this.energySource;
}
/**
* 清空合成网格,材料一键存入 ME 网络(对应终端 S_STASH 按钮与"关闭时自动清理合成槽"设置)。
* 仅客户端调用:发送专用 C2S 包,服务端直接执行 {@link #clearGridToNetworkOnServer()}
* (不走 AE2 quickMove/MOVE_REGION 语义链,任何环境下都可靠)。
*/
public void clearCraftingGrid() {
if (!isClientSide()) {
return;
}
PacketDistributor.sendToServer(
new ArcaneTerminalClearC2SPacket(this.containerId, ArcaneTerminalClearC2SPacket.ACTION_STASH, false));
}
/** 服务端:把合成网格所有物品存入 ME 网络,放不下的回玩家背包。结果槽由 slotsChanged 联动刷新。 */
public void clearGridToNetworkOnServer() {
var grid = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
var playerInv = new PlayerInternalInventory(getPlayerInventory());
boolean changed = false;
for (int i = 0; i < grid.size(); i++) {
ItemStack stack = grid.getStackInSlot(i);
if (stack.isEmpty()) continue;
changed = true;
long inserted = 0;
AEItemKey key = AEItemKey.of(stack);
if (key != null && canInteractWithGrid()) {
inserted = StorageHelper.poweredInsert(energySource, storage, key, stack.getCount(),
this.getActionSource());
}
if (inserted >= stack.getCount()) {
grid.setItemDirect(i, ItemStack.EMPTY);
} else {
ItemStack remaining = stack.copy();
remaining.shrink((int) inserted);
ItemStack left = playerInv.addItems(remaining);
grid.setItemDirect(i, left.isEmpty() ? ItemStack.EMPTY : left);
}
}
if (changed) {
slotsChanged(grid.toContainer());
}
}
/** 服务端:客户端上报的"关闭时自动清理合成槽"开关值(在菜单构造后由 C2S 包设置)。 */
private boolean clearOnClose = false;
public void setClearOnClose(boolean clearOnClose) {
this.clearOnClose = clearOnClose;
}
/**
* 菜单关闭时(无论 ESC、走开、方块被拆、服务端强制关闭),按客户端上报的开关清理网格。
* 不依赖 GUI onClose 是否触发——这是最可靠的兜底。
*/
@Override
public void removed(net.minecraft.world.entity.player.Player player) {
if (!isClientSide() && clearOnClose) {
clearGridToNetworkOnServer();
}
super.removed(player);
}
private static final String ACTION_CLEAR_TO_PLAYER = "clearToPlayer";
/**
* 清空合成网格,材料一键存入玩家背包(对应终端 S_STASH_TO_PLAYER_INV 按钮)。
* 复刻 {@code appeng.menu.me.items.CraftingTermMenu.clearToPlayerInventory}。
*/
public void clearToPlayerInventory() {
if (isClientSide()) {
sendClientAction(ACTION_CLEAR_TO_PLAYER);
return;
}
var craftingGridInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
var playerInv = new PlayerInternalInventory(getPlayerInventory());
for (int i = 0; i < craftingGridInv.size(); ++i) {
for (int emptyLoop = 0; emptyLoop < 2; ++emptyLoop) {
boolean allowEmpty = emptyLoop == 1;
// 快捷栏优先
final int HOTBAR_SIZE = 9;
for (int j = HOTBAR_SIZE; j-- > 0;) {
if (playerInv.getStackInSlot(j).isEmpty() == allowEmpty) {
craftingGridInv.setItemDirect(i,
playerInv.getSlotInv(j).addItems(craftingGridInv.getStackInSlot(i)));
}
}
// 其余背包
for (int j = HOTBAR_SIZE; j < Inventory.INVENTORY_SIZE; ++j) {
if (playerInv.getStackInSlot(j).isEmpty() == allowEmpty) {
craftingGridInv.setItemDirect(i,
playerInv.getSlotInv(j).addItems(craftingGridInv.getStackInSlot(i)));
}
}
}
}
}
// ========== 配方检测 ==========
@Override
public void slotsChanged(Container inventory) {
ThaumicEnergistics.LOG.debug("[A] slotsChanged fired, beingTaken={}, inventoryClass={}",
isCraftingResultBeingTaken, inventory.getClass().getSimpleName());
if (!isCraftingResultBeingTaken) {
updateCurrentRecipeAndOutput(false);
} else {
ThaumicEnergistics.LOG.debug("[A] slotsChanged SKIPPED (beingTaken=true)");
}
}
public void setCraftingResultBeingTaken(boolean beingTaken) {
ThaumicEnergistics.LOG.debug("[A] setCraftingResultBeingTaken={}", beingTaken);
this.isCraftingResultBeingTaken = beingTaken;
}
@@ -195,29 +295,22 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
private boolean hasEnoughVis(AspectList cost) {
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
if (cost == null || cost.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] no cost, no wand needed");
return true;
}
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0);
ThaumicEnergistics.LOG.debug("[V] wandStack={}", wandStack);
if (wandStack.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] wandStack EMPTY");
return false;
}
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
ThaumicEnergistics.LOG.debug("[V] not a WandCastingItem");
return false;
}
var centivisCost = new AspectList();
for (var aspect : cost.aspects()) {
centivisCost.add(aspect, cost.amount(aspect) * CENTIVIS);
}
boolean result = wand.consumeVisCost(wandStack, getPlayerInventory().player, centivisCost, false, true);
ThaumicEnergistics.LOG.debug("[V] consumeVisCost result={}", result);
return result;
return wand.consumeVisCost(wandStack, getPlayerInventory().player, centivisCost, false, true);
}
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
@@ -364,6 +457,98 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return new MissingIngredientSlots(missingSlots, craftableSlots);
}
/**
* 为 JEI "+" 填充挑选一个可用的材料模板:优先取 ME 网络中实际有货的变体,
* 其次玩家背包有货的变体,再次可自动合成的变体,最后回退到第一个变体。
* <p>
* 背景:奥术配方 recipeId=null,服务端只能用客户端模板重建单物品 Ingredient,
* 若客户端固定发第一个变体(如石头),网络里只有深板岩/圆石时槽位就拉不到东西。
* 这里带预留计数(reservedAmounts),多个槽位需要同一材料时不会重复占用同一份库存。
*
* @param stored 网络库存快照(AEItemKey → 数量),由调用方一次构建,避免每槽全量扫描
* @param craftable 网络中可自动合成的物品集合快照
*/
public ItemStack pickAvailableTemplate(Ingredient ingredient, Map<AEItemKey, Long> stored,
Set<AEItemKey> craftable, Object2IntOpenHashMap<AEItemKey> reservedAmounts) {
ItemStack[] items = ingredient.getItems();
if (items.length == 0) {
return ItemStack.EMPTY;
}
// 1) ME 网络中有库存的变体
for (ItemStack variant : items) {
AEItemKey key = AEItemKey.of(variant);
if (key == null) continue;
long avail = stored.getOrDefault(key, 0L) - reservedAmounts.getOrDefault(key, 0);
if (avail >= 1) {
reservedAmounts.merge(key, 1, Integer::sum);
return variant.copyWithCount(1);
}
}
// 2) 玩家背包中的变体
var playerItems = getPlayerInventory().items;
for (int i = 0; i < playerItems.size(); i++) {
if (isPlayerInventorySlotLocked(i)) continue;
var stack = playerItems.get(i);
if (stack.isEmpty() || !ingredient.test(stack)) continue;
AEItemKey key = AEItemKey.of(stack);
int used = key != null ? reservedAmounts.getOrDefault(key, 0) : 0;
if (stack.getCount() - used >= 1) {
if (key != null) {
reservedAmounts.merge(key, 1, Integer::sum);
}
return stack.copyWithCount(1);
}
}
// 3) 可自动合成的变体(仅当玩家按住 Ctrl 时服务端才会真正排产;不按则槽位保持空,与旧行为一致)
for (ItemStack variant : items) {
AEItemKey key = AEItemKey.of(variant);
if (key != null && craftable.contains(key) && reservedAmounts.getOrDefault(key, 0) == 0) {
reservedAmounts.merge(key, 1, Integer::sum);
return variant.copyWithCount(1);
}
}
// 4) 回退:第一个变体
return items[0].copyWithCount(1);
}
/**
* 构建 ME 网络库存快照(AEItemKey → 存储量)。供 JEI "+" 模板挑选一次性调用。
*/
public Map<AEItemKey, Long> snapshotStoredAmounts() {
Map<AEItemKey, Long> map = new HashMap<>();
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var entry : clientRepo.getAllEntries()) {
if (!(entry.getWhat() instanceof AEItemKey itemKey)) continue;
if (entry.getStoredAmount() > 0) {
map.put(itemKey, entry.getStoredAmount());
}
}
}
return map;
}
/**
* 构建网络中可自动合成物品的集合快照。供 JEI "+" 模板挑选一次性调用。
*/
public Set<AEItemKey> snapshotCraftableKeys() {
Set<AEItemKey> set = new HashSet<>();
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var entry : clientRepo.getAllEntries()) {
if (!(entry.getWhat() instanceof AEItemKey itemKey)) continue;
if (entry.isCraftable()) {
set.add(itemKey);
}
}
}
return set;
}
/**
* 检查 ME 网络中是否有指定材料的库存(含已预留量)。
* 直接遍历 clientRepo 条目匹配 ingredient,确保不受 linkStatus 时序或 getByIngredient 实现细节影响。
@@ -23,7 +23,6 @@ import thaumcraft.api.aspects.Aspect;
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;
@@ -234,8 +233,11 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
/**
* Encode 按钮可用的条件:源物品已配置 + 已选要素 + 空白样板槽有样板 + 成品槽为空。
* 服务端从 Tile 读(权威);客户端 encoder 为 null,从同步的槽内容判断。
* 先执行 getSourceAspects():重开 GUI 后源物品仍在 Tile 中,但本容器缓存为空,
* 不先解析会误判"未选要素"导致按钮置灰/编码无效。
*/
public boolean canEncode() {
getSourceAspects();
if (getSelectedAspect() == null) return false;
if (getSourceItem().isEmpty()) return false;
if (encoder != null) {
@@ -251,10 +253,12 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
// 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板
if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) {
ThaumicEnergistics.LOG.info("[DistillationEncoder] 玩家未解锁研究 {},拒绝编码蒸馏样板", RESEARCH_DISTILESSENTIA);
return;
}
// 重开 GUI 后服务端缓存为空:先从 Tile 的源物品重新解析要素,
// 否则 getSelectedAspect() 恒为 null(客户端已选、服务端无缓存 → 点击 Encode 无效)。
getSourceAspects();
Aspect selected = getSelectedAspect();
ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
@@ -11,6 +11,9 @@ import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import appeng.api.upgrades.IUpgradeInventory;
import appeng.api.upgrades.UpgradeInventories;
import appeng.core.definitions.AEItems;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
@@ -32,6 +35,11 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
private static final int PLAYER_INV_END = 28;
private static final int HOTBAR_START = 28;
private static final int HOTBAR_END = 37;
/** cell 升级卡槽(3 格,容器槽位 37..39,位于玩家槽之后)。 */
private static final int UPGRADE_X = 98;
private static final int UPGRADE_Y = 8;
private static final int UPGRADE_COUNT = 3;
private static final int UPGRADE_START = HOTBAR_END;
private final TileEssentiaCellWorkbench workbench;
@@ -50,6 +58,80 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
addSlot(new SlotItemHandler(cellHandler, 0, 152, 8));
for (int r = 0; r < 3; r++) for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c + r * 9 + 9, 8 + c * 18, 167 + r * 18));
for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c, 8 + c * 18, 225));
// ===== cell 升级卡槽(3 格,动态绑定当前 cell 的升级库存,卡片持久化在 cell 物品上) =====
IItemHandler cellUpgrades = new CellUpgradeHandler(this::currentCellUpgrades);
for (int i = 0; i < UPGRADE_COUNT; i++) {
addSlot(new SlotItemHandler(cellUpgrades, i, UPGRADE_X + i * 18, UPGRADE_Y) {
@Override
public int getMaxStackSize() {
return 1;
}
@Override
public boolean mayPlace(ItemStack s) {
return AEItems.CAPACITY_CARD.is(s) || AEItems.FUZZY_CARD.is(s);
}
@Override
public void setChanged() {
super.setChanged();
if (workbench != null) {
workbench.setChanged();
}
}
});
}
}
/** 当前 cell 的升级库存;无 cell 时返回空库存。 */
private IUpgradeInventory currentCellUpgrades() {
if (workbench != null && workbench.hasCell()) {
ItemStack cell = workbench.getCell();
if (cell.getItem() instanceof ItemEssentiaCell cellItem) {
return cellItem.getUpgrades(cell);
}
}
return UpgradeInventories.empty();
}
/** 把 AE2 InternalInventory(IUpgradeInventory)适配成 NeoForge IItemHandler,供 SlotItemHandler 使用。 */
private static final class CellUpgradeHandler implements IItemHandler {
private final java.util.function.Supplier<IUpgradeInventory> supplier;
CellUpgradeHandler(java.util.function.Supplier<IUpgradeInventory> supplier) {
this.supplier = supplier;
}
@Override
public int getSlots() {
return supplier.get().size();
}
@Override
public ItemStack getStackInSlot(int slot) {
return supplier.get().getStackInSlot(slot);
}
@Override
public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) {
return supplier.get().insertItem(slot, stack, simulate);
}
@Override
public ItemStack extractItem(int slot, int amount, boolean simulate) {
return supplier.get().extractItem(slot, amount, simulate);
}
@Override
public int getSlotLimit(int slot) {
return supplier.get().getSlotLimit(slot);
}
@Override
public boolean isItemValid(int slot, ItemStack stack) {
return supplier.get().isItemValid(slot, stack);
}
}
/** 客户端打开菜单用(IMenuTypeExtension 注册);workbench 为 null,分区操作走 C2S 到服务端。 */
@@ -134,6 +216,9 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
// 玩家背包 → cell 槽(仅当 cell 槽空且物品是源质元件)
if (stack.getItem() instanceof ItemEssentiaCell && workbench.getCell().isEmpty()) {
if (!moveItemStackTo(stack, SLOT_CELL, SLOT_CELL + 1, false)) return ItemStack.EMPTY;
} else if (AEItems.CAPACITY_CARD.is(stack) || AEItems.FUZZY_CARD.is(stack)) {
// 容量/模糊卡 → cell 升级槽
if (!moveItemStackTo(stack, UPGRADE_START, UPGRADE_START + UPGRADE_COUNT, false)) return ItemStack.EMPTY;
} else {
// 含源质物品 → 尝试添加分区(物品不消费,仅添加分区)
ResourceLocation aspectId = getAspectFromItem(stack);
@@ -142,6 +227,9 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
}
return ItemStack.EMPTY;
}
} else {
// 升级卡槽 → 玩家背包
if (!moveItemStackTo(stack, PLAYER_INV_START, HOTBAR_END, true)) return ItemStack.EMPTY;
}
if (stack.isEmpty()) {
@@ -33,7 +33,6 @@ import thaumicenergistics.common.tiles.TileKnowledgeInscriber;
import thaumicenergistics.init.ModItems;
import thaumicenergistics.init.ModMenuTypes;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.ThaumicEnergistics;
public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
@@ -133,6 +132,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
kCoreHandler.open(slotStack);
updatePatternSlots();
}
// 服务端构造后立即同步按钮状态到客户端(否则客户端 saveStateSlot 一直为初始 0 → 按钮永远无效)
sendSaveState();
}
private void initPatternSlots() {
@@ -181,6 +182,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
}
syncKCoreToTile();
// 放/取核心后同步按钮状态(可保存/可删除/无效)到客户端
sendSaveState();
}
private void clearPatternSlots() {
@@ -372,7 +375,6 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
String research = activeRecipe.getResearch();
if (research != null && !research.isEmpty()
&& !TCResearchManager.has(playerInv.player, research)) {
ThaumicEnergistics.LOG.info("[KnowledgeInscriber] 玩家未解锁研究 {},拒绝保存奥术样板", research);
return;
}
@@ -19,6 +19,7 @@ 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.ThEEssentiaContainers;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
@@ -83,10 +84,12 @@ public final class EssentiaFillHelper {
return true;
}
// 罐子(IEssentiaContainerItem):每次只装 1 个空罐(手堆叠 -1,背包得 1 满罐)
if (!(carried.getItem() instanceof IEssentiaContainerItem container)) {
// 罐子(JarBlockItem):每次只装 1 个空罐(手堆叠 -1,背包得 1 满罐)。
// 其余 IEssentiaContainerItem(天域之华等)不参与装罐。
if (!ThEEssentiaContainers.isJar(carried)) {
return false;
}
IEssentiaContainerItem container = (IEssentiaContainerItem) carried.getItem();
AspectList existing = container.getAspects(carried);
if (existing != null && !existing.isEmpty()) {
return false; // 罐子非空,不能装
@@ -133,9 +136,13 @@ public final class EssentiaFillHelper {
@Nullable
public static ItemStack extractContainerContent(MEStorage storage, IEnergySource energySource,
IActionSource actionSource, ItemStack stack) {
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) {
// 仅要素安瓿/要素罐子(天域之华、水晶等其他 IEssentiaContainerItem 一律不处理)
if (!ThEEssentiaContainers.isEssentiaContainer(stack)) {
return null;
}
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) {
return stack; // 空玻璃安瓿:无源质可存
}
int count = stack.getCount();
if (count <= 0) {
return stack;
@@ -6,20 +6,17 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import appeng.client.gui.Icon;
import thaumicenergistics.ThaumicEnergistics;
import org.anti_ad.mc.ipn.api.IPNIgnore;
/**
* 奥术装配器 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
*
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
* 第1块 主矩形:(0,0) → 176×197
* 第2块 右上不规则:(179,0) → 上部32w(0~15y),下部55w(16~103y)
* 第3块 右下护甲区:(180,129) → 68×68
* 第4块 底部7个Vis条:每个 4×16 像素,起始(42,202),步长18(42-45, 60-63, 78-81...)
*
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
* 新版贴图 arcane_assembler.png = 256×256 整张背景。
* 进度条填充材质在纹理 y=240(7 条 4×16,x 步长 5:0,5,10,15,20,25,30)。
*/
@IPNIgnore
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
@@ -27,66 +24,87 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png");
// ===== 第4块 Vis条 =====
/** 纹理中空槽(暗色背景)的 Y 坐标 = 进度条在 GUI 中的显示位置 */
private static final int VIS_EMPTY_Y = 87;
/** 纹理中满亮条的 Y 坐标(裁剪源,不直接显示) */
private static final int VIS_FULL_Y = 202;
private static final int VIS_BAR_W = 4;
private static final int VIS_BAR_H = 16;
private static final int VIS_BAR_STEP = 18;
/** 贴图规范起始 X = 41(步长18:41,59,77,95,113,131,149) */
private static final int VIS_BAR_X = 41;
private static final int BAR_W = 4;
private static final int BAR_H = 16;
/** 进度条填充材质纹理 y。 */
private static final int BAR_FILL_Y = 240;
/** 7 条进度条:{填充材质 x, 目标 x, 目标 y},顺序 风/水/火/秩序/混沌/地/合成。 */
private static final int[][] BARS = {
{ 0, 8, 81 }, // 风 AIR
{ 5, 8, 99 }, // 水 WATER
{ 10, 8, 117 }, // 火 FIRE
{ 15, 15, 81 }, // 秩序 ORDER
{ 20, 15, 99 }, // 混沌 ENTROPY
{ 25, 15, 117 },// 地 EARTH
{ 30, 141, 99 } // 合成条
};
/** 配方预览 3×3 起点(16×16 格,步距 18)。 */
private static final int PREVIEW_X = 26;
private static final int PREVIEW_Y = 81;
public GuiArcaneAssembler(ContainerArcaneAssembler menu, Inventory inv, Component title) {
super(menu, inv, title);
this.imageWidth = 254;
this.imageHeight = 226;
this.inventoryLabelY = 115 - 12;
this.titleLabelY = 5;
// 主区按 0~175 取中轴线 87.5 居中(imageWidth=175,MC 自动按 175 居中);
// 右侧 176~196 作为附加(右上角块),blit 整图时自然延伸,不占主区宽度。
this.imageWidth = 175;
this.imageHeight = 231;
// 物品栏标题下移(136→137)
this.inventoryLabelY = 137;
// 奥术装配器标题上移(4→3)
this.titleLabelY = 3;
}
@Override
protected void renderBg(GuiGraphics g, float partialTicks, int mx, int my) {
int left = (this.width - this.imageWidth) / 2;
int top = (this.height - this.imageHeight) / 2;
int left = (this.width - this.imageWidth) / 2;
int top = (this.height - this.imageHeight) / 2;
// ===== 分块绘制纹理(仿1.7.10,bar区域不参与blit) =====
// 整图渲染(0~196,含右侧右上角附加),主区 0~175 已居中
g.blit(TEX, left, top, 0, 0, 196, imageHeight);
// 第1块:主矩形 176×197
g.blit(TEX, left, top, 0, 0, 176, 197);
// ===== 空槽图标:加速卡(4 格) =====
for (int i = 0; i < 4; i++) {
Slot slot = this.menu.getUpgradeSlot(i);
if (!slot.hasItem()) {
Icon.BACKGROUND_UPGRADE.getBlitter().dest(left + slot.x, top + slot.y).blit(g);
}
}
// 第2块:右上不规则区域(上部 32×16 + 下部 55×88)
g.blit(TEX, left + 179, top, 179, 0, 32, 16);
g.blit(TEX, left + 179, top + 16, 179, 16, 55, 88);
// ===== 配方预览:3×3 材料 + 预产物(数据由 AssemblerPreviewSyncPacket 同步到菜单) =====
if (this.menu.clientPreviewActive) {
for (int i = 0; i < 9; i++) {
ItemStack stack = this.menu.clientPreviewGrid[i];
if (stack == null || stack.isEmpty()) continue;
int col = i % 3, row = i / 3;
g.renderItem(stack, left + PREVIEW_X + col * 18, top + PREVIEW_Y + row * 18);
}
// 预产物:16×16 居中于 21×21 大格子(113,96 ~ 134,117)
ItemStack result = this.menu.clientPreviewResult;
if (!result.isEmpty()) {
g.renderItem(result, left + 115, top + 98);
}
}
// 第3块:右下正方形 68×68
g.blit(TEX, left + 180, top + 129, 180, 129, 68, 68);
// ===== 第4块:Vis条(1.7.10 裁剪方式:Y=87 空槽背景 + Y=202 亮条裁剪) =====
// 数据源:容器同步(menu.visData,服务端每 tick 同步 storedVis/isCrafting/进度——不依赖 BE 网络同步)
// ===== 进度条:6 要素 + 1 合成条(填充材质 y=240,按比例从底部向上裁剪) =====
ContainerData data = this.menu.visData;
final int maxCvis = 187 * 10;
for (int i = 0; i < 7; i++) {
int bx = left + VIS_BAR_X + i * VIS_BAR_STEP;
int by = top + VIS_EMPTY_Y;
for (int i = 0; i < BARS.length; i++) {
int srcX = BARS[i][0];
int bx = left + BARS[i][1];
int by = top + BARS[i][2];
// 从纹理 Y=87 裁剪空槽背景
g.blit(TEX, bx, by, VIS_BAR_X + i * VIS_BAR_STEP, VIS_EMPTY_Y, VIS_BAR_W, VIS_BAR_H);
float ratio = 0f;
float ratio;
if (i < 6) {
// 1-6格:源质充能比例
int cvis = data.get(ContainerArcaneAssembler.DATA_AIR + i);
ratio = Math.min(1f, (float) cvis / maxCvis);
} else {
// 第7格:非合成时要素接入常亮,合成时显示渐变进度
if (data.get(ContainerArcaneAssembler.DATA_CRAFTING) == 1) {
int tick = data.get(ContainerArcaneAssembler.DATA_CRAFT_TICK);
int total = Math.max(1, data.get(ContainerArcaneAssembler.DATA_TICKS_PER_CRAFT));
ratio = Math.min(1f, (float) tick / total);
} else {
// 要素接入达成:任一原质充入(源质已从网络流入)→ 常亮
boolean anyVis = false;
for (int a = 0; a < 6; a++) {
if (data.get(ContainerArcaneAssembler.DATA_AIR + a) > 0) { anyVis = true; break; }
@@ -95,20 +113,12 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
}
}
// 从纹理 Y=202 裁剪满亮条,按高度百分比叠加
int fillHeight = Math.round(VIS_BAR_H * ratio);
int fillHeight = Math.round(BAR_H * ratio);
if (fillHeight > 0) {
int srcX = VIS_BAR_X + i * VIS_BAR_STEP;
int srcY = VIS_FULL_Y + (VIS_BAR_H - fillHeight);
int dstY = by + (VIS_BAR_H - fillHeight);
g.blit(TEX, bx, dstY, srcX, srcY, VIS_BAR_W, fillHeight);
int srcY = BAR_FILL_Y + (BAR_H - fillHeight);
int dstY = by + (BAR_H - fillHeight);
g.blit(TEX, bx, dstY, srcX, srcY, BAR_W, fillHeight);
}
// bar 边框
g.fill(bx - 1, by - 1, bx, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx + VIS_BAR_W, by - 1, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx - 1, by - 1, bx + VIS_BAR_W + 1, by, 0xAA000000);
g.fill(bx - 1, by + VIS_BAR_H, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
}
}
@@ -118,4 +128,4 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
super.render(g, mx, my, pt);
this.renderTooltip(g, mx, my);
}
}
}
@@ -3,11 +3,16 @@ package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.api.config.ActionItems;
import appeng.client.gui.widgets.ActionButton;
import appeng.core.AEConfig;
import appeng.menu.slot.AppEngCraftingSlot;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.client.gui.AspectGuiRenderer;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.style.ScreenStyle;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.ArcaneTerminalClearC2SPacket;
import java.util.Map;
@@ -24,6 +29,60 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
public GuiArcaneCraftingTerminal(ContainerArcaneCraftingTerminal menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
// 一键存入 ME 网络(合成格清空)
ActionButton clearBtn = new ActionButton(ActionItems.S_STASH, btn -> menu.clearCraftingGrid());
clearBtn.setHalfSize(true);
clearBtn.setDisableBackground(true);
widgets.add("clearCraftingGrid", clearBtn);
// 一键存入玩家背包(合成格清空)
ActionButton clearToPlayerInvBtn = new ActionButton(ActionItems.S_STASH_TO_PLAYER_INV,
btn -> menu.clearToPlayerInventory());
clearToPlayerInvBtn.setHalfSize(true);
clearToPlayerInvBtn.setDisableBackground(true);
widgets.add("clearToPlayerInv", clearToPlayerInvBtn);
// 打开终端时上报 AE2 设置"关闭时自动清理合成槽"——服务端在菜单 removed 时兜底清理
sendClearOnCloseSetting();
}
/**
* 主区(0~175)按中轴线 87.5 居中:AE2 终端 imageWidth=194(srcRect),默认 leftPos 按 194 居中会让主区偏左约 10px。
* 覆写 leftPos 用 175 居中,右侧 176~194(view cells 区)作为附加延伸到右侧。
* <p>
* 关键时序:AEBaseScreen.init 先调 AbstractContainerScreen.init(按 imageWidth 算 leftPos),随后用该 leftPos
* 一次性解析并冻结按钮/文本层坐标(populateScreen);而渲染时贴图与槽位按当前 leftPos 画。若把 175 居中放在
* super.init() 之后,按钮层基准是 194 居中、贴图层是 175 居中,差 (194-175)/2≈10px,按钮层就像整体左移。
* 因此先把 imageWidth 临时改为 175,让 super.init() 全程内部就用 87.5 居中解析所有层,再恢复。
*/
@Override
public void init() {
int originalImageWidth = this.imageWidth;
this.imageWidth = 175;
super.init();
this.imageWidth = originalImageWidth;
this.leftPos = (this.width - 175) / 2;
}
/** 向服务端上报当前 AE2 全局设置"关闭时自动清理合成槽"。 */
private void sendClearOnCloseSetting() {
boolean enabled = AEConfig.instance().isClearGridOnClose();
PacketDistributor.sendToServer(new ArcaneTerminalClearC2SPacket(
this.menu.containerId, ArcaneTerminalClearC2SPacket.ACTION_SET_CLEAR_ON_CLOSE, enabled));
}
/**
* 关闭终端时按 AE2 全局设置"自动清理合成槽"清空合成网格。
* 双保险:先上报最新开关值,若开启再立即清一次(removed 兜底仍会执行,网格已空则无副作用)。
*/
@Override
public void onClose() {
sendClearOnCloseSetting();
if (AEConfig.instance().isClearGridOnClose()) {
this.getMenu().clearCraftingGrid();
}
super.onClose();
}
@Override
@@ -15,6 +15,10 @@ public class GuiDistillationPatternEncoder extends AbstractContainerScreen<Conta
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/distillation_encoder.png");
/** Encode 按钮材质:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。 */
private static final ResourceLocation BUTTON_TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/button.png");
private static final int GUI_WIDTH = 176;
private static final int GUI_HEIGHT = 234;
@@ -35,11 +39,30 @@ public class GuiDistillationPatternEncoder extends AbstractContainerScreen<Conta
int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2;
// Encode 按钮:对齐 1.7.10(146,94),位于 encoded 槽上方,不覆盖 selected 槽 (116,69);
// Encode 按钮:对齐 1.7.10(146,94),位于 encoded 槽上方;用 button.png 材质(常态/悬停),等比缩放容纳文字。
// 仅当配置完成(源物品+选中要素+空白样板就绪)时可点击
encodeButton = Button.builder(Component.literal("Encode"), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0);
}).pos(left + 140, top + 94).size(34, 16).build();
encodeButton = new Button(
left + 140, top + 94, 34, 14, Component.translatable("thaumicenergistics.gui.encode"),
btn -> this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0),
b -> Component.empty()) {
@Override
protected void renderWidget(GuiGraphics g, int mx, int my, float pt) {
// 仅鼠标悬停/按下显示第二帧(避免焦点导致一直蓝色)
int v = this.isHovered() ? 15 : 0;
// 源 (0,v,32,13) 等比拉伸到按钮 (34×14)
g.blit(BUTTON_TEX, this.getX(), this.getY(), this.width, this.height, 0, v, 32, 13, 32, 32);
// 按钮文字居中
String s = this.getMessage().getString();
if (!s.isEmpty()) {
var font = net.minecraft.client.Minecraft.getInstance().font;
int tw = font.width(s);
g.drawString(font, s,
this.getX() + (this.width - tw) / 2,
this.getY() + (this.height - 8) / 2,
0xFFFFFF, false);
}
}
};
addRenderableWidget(encodeButton);
}
@@ -4,10 +4,7 @@ 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 thaumicenergistics.common.integration.tc.ThEEssentiaContainers;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.style.ScreenStyle;
@@ -41,13 +38,14 @@ public class GuiEssentiaTerminal extends MEStorageScreen<EssentiaTerminalMenu> {
// 源质物品来源:优先菜单 carried;为空时回退玩家主手物品
ItemStack sourceStack = !carried.isEmpty() ? carried
: this.menu.getPlayerInventory().player.getMainHandItem();
boolean isContainer = isSourceContainer(sourceStack);
boolean containerHasEssentia = isContainer && getContainerEssentia(sourceStack) != null;
boolean isContainer = ThEEssentiaContainers.isEssentiaContainer(sourceStack);
boolean containerHasEssentia = isContainer && ThEEssentiaContainers.hasEssentia(sourceStack);
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) {
var slotStack = this.hoveredSlot.getItem();
if (slotStack.getItem() instanceof IEssentiaContainerItem) {
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
this.menu.containerId, this.hoveredSlot.getSlotIndex(), slotStack));
return true;
@@ -94,25 +92,4 @@ public class GuiEssentiaTerminal extends MEStorageScreen<EssentiaTerminalMenu> {
}
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;
}
}
@@ -17,11 +17,15 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/knowledge_inscriber.png");
private static final int GUI_WIDTH = 210;
/** 保存/删除样板按钮材质:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。 */
private static final ResourceLocation BUTTON_TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/button.png");
// 主区宽度 175(中轴线 87.5,主区对称居中);知识核心槽在右侧单独渲染(x=176~209 边栏),不计入主区宽度。
private static final int GUI_WIDTH = 175;
private static final int GUI_HEIGHT = 244;
private Button saveButton;
private Button clearButton;
private ContainerKnowledgeInscriber.CoreSaveState saveState =
ContainerKnowledgeInscriber.CoreSaveState.Disabled_MissingCore;
@@ -40,48 +44,65 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2;
saveButton = Button.builder(Component.literal(""), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0);
}).pos(left + 141, top + 109).size(38, 18).build();
// 保存/删除样板按钮:放预览产物槽(结果槽 116,108)上方,等比 32×13。
// 按钮中心与结果槽中心(x=125)关于同一 y 轴对称 → 按钮 x = 125-16 = 109。
// 材质 button.png:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。
saveButton = new Button(
left + 109, top + 90, 32, 13, Component.empty(),
btn -> this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0),
b -> Component.empty()) {
@Override
protected void renderWidget(GuiGraphics g, int mx, int my, float pt) {
// 仅鼠标悬停/按下显示第二帧;用 isHovered() 而非 isHoveredOrFocused()——
// 后者在按钮获得键盘焦点(点击后)时也 true,会导致写入配方后一直保持蓝色
int v = this.isHovered() ? 15 : 0;
g.blit(BUTTON_TEX, this.getX(), this.getY(), 0, v, this.width, this.height, 32, 32);
// 按钮文字居中
String s = this.getMessage().getString();
if (!s.isEmpty()) {
var font = net.minecraft.client.Minecraft.getInstance().font;
int tw = font.width(s);
g.drawString(font, s,
this.getX() + (this.width - tw) / 2,
this.getY() + (this.height - 8) / 2,
0xFFFFFF, false);
}
}
};
addRenderableWidget(saveButton);
clearButton = Button.builder(Component.literal("C"), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 1);
}).pos(left + 80, top + 89).size(8, 8).build();
addRenderableWidget(clearButton);
// 立即设置按钮文字/激活(F11 全屏切换会重建按钮,init 里必须重设,否则文字丢失)
updateSaveButton();
}
@Override
protected void containerTick() {
super.containerTick();
ContainerKnowledgeInscriber.CoreSaveState newState = menu.getCurrentSaveState();
if (newState != saveState) {
saveState = newState;
updateSaveButton();
}
saveState = menu.getCurrentSaveState();
// 每 tick 重设(而非仅状态变化时):F11 resize 重建按钮后也能恢复文字/激活
updateSaveButton();
}
private void updateSaveButton() {
switch (saveState) {
case Disabled_MissingCore:
saveButton.active = false;
saveButton.setMessage(Component.literal("No Core"));
saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.no_core"));
break;
case Disabled_InvalidRecipe:
saveButton.active = false;
saveButton.setMessage(Component.literal("Invalid"));
saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.invalid"));
break;
case Disabled_CoreFull:
saveButton.active = false;
saveButton.setMessage(Component.literal("Full"));
saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.full"));
break;
case Enabled_Save:
saveButton.active = true;
saveButton.setMessage(Component.literal("Save"));
saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.save"));
break;
case Enabled_Delete:
saveButton.active = true;
saveButton.setMessage(Component.literal("Delete"));
saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.delete"));
break;
}
}
@@ -90,7 +111,10 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
protected void renderBg(GuiGraphics g, float pt, int mx, int my) {
int x = (width - imageWidth) / 2;
int y = (height - imageHeight) / 2;
g.blit(TEX, x, y, 0, 0, imageWidth, imageHeight);
// 主区(0~175)对称居中
g.blit(TEX, x, y, 0, 0, GUI_WIDTH, imageHeight);
// 右侧知识核心槽边栏(x=176~209)单独渲染,不占主区宽度
g.blit(TEX, x + 176, y, 176, 0, 34, imageHeight);
}
@Override
@@ -52,6 +52,10 @@ public final class RecipeRegistration {
public static ShapelessArcaneRecipe ARCANE_CRAFTING_TERMINAL;
public static ShapelessArcaneRecipe VIS_INTERFACE;
public static ShapelessArcaneRecipe ESSENTIA_VIBRATION_CHAMBER;
/** 源质无线接收器(无序奥术)。 */
public static ShapelessArcaneRecipe ESSENTIA_PROVIDER_CONNECTION;
/** 源质无线绑定工具(无序奥术)。 */
public static ShapelessArcaneRecipe WIRELESS_CONNECTOR;
/** 赛特斯石英复制(坩埚炼金配方——研究页关联)。 */
public static CrucibleRecipe CERTUS_DUPE_RECIPE;
@@ -83,6 +87,7 @@ public final class RecipeRegistration {
new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.EXCHANGE, 5));
}
// ==================== 奥术有序配方 ====================
private static void registerShapedArcane() {
STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_1K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1),
@@ -210,6 +215,8 @@ public final class RecipeRegistration {
}
// ==================== 奥术无序配方 ====================
private static void registerShapelessArcane() {
// 1. 扩散核心
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
@@ -265,6 +272,22 @@ public final class RecipeRegistration {
tc("jar_of_essentia"),
stack(ModItems.COALESCENCE_CORE));
// 8. 源质无线接收器(无序奥术,研究:源质供应器)
ESSENTIA_PROVIDER_CONNECTION = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAPROVIDER",
new ItemStack(ModBlocks.ESSENTIA_PROVIDER_CONNECTION.get(), 2),
new AspectList().add(Aspect.ORDER, 8).add(Aspect.AIR, 6).add(Aspect.FIRE, 3),
stack(ModItems.COALESCENCE_CORE),
stack(ModItems.DIFFUSION_CORE),
ae("wireless_receiver"),
tc("balanced_shard"));
// 9. 源质无线绑定工具(无序奥术,研究:源质供应器)
WIRELESS_CONNECTOR = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAPROVIDER",
stack(ModItems.WIRELESS_CONNECTOR),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
ae("certus_quartz_wrench"),
tc("balanced_shard"));
}
@@ -21,8 +21,11 @@ import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/**
* 源质网格服务的实现。查询和提取源质时同时检查两个存储后端:
* Path A: EssentiaMonitor(注魔供应器/傀儡背包直接写入),
* Path B: AE2 原生存储(源质存储元件、输入总线等)。
* Path A: EssentiaMonitor(历史遗留的内存镜像,现仅参与读取),
* Path B: AE2 原生存储(源质存储元件、存储总线等)。
* <p>
* 写入只走 Path B:网络装不下的部分如实返回"未接受",不再回退进内存 monitor
* (否则网络没有源质存储时也会照吸,源质既不在 ME 存储里也不持久化,等于消失)。
*/
public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider {
@@ -101,18 +104,13 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider
@Override
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
if (amount <= 0) return 0;
// 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化)
// 只承认 AE2 原生存储(源质存储元件 / 存储总线)真正接收的量。
// 网络没有源质存储或已满时如实返回"未接受",调用方(输入总线 / 无线接收器 /
// 源质供应器 / 傀儡背包)据此把源质留在原容器里,不抽走。
// 旧实现把装不下的部分塞进内存 monitor 兜底 —— 等于"网络无存储也照吸",
// 而这些源质不在任何 ME 存储里、不随存档持久化,表现为源质凭空消失。
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 amount - nativeInjected;
}
private long injectNative(ResourceLocation aspectId, long amount, boolean simulate) {
@@ -0,0 +1,74 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* 奥术装配器 Jade 显示:
* <ul>
* <li>网络状态:在线(节点 active)/ 无频道(有能量)/ 离线(无能量)</li>
* <li>要素状态:已连接(内存卡配置了 vis 源频率)/ 未连接</li>
* </ul>
* 服务端把状态写入 NBT,客户端渲染。
*/
public enum AssemblerProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
private static final String TAG_VIS = "VisConnected";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileArcaneAssembler assembler) {
data.putBoolean(TAG_ONLINE, assembler.isActive());
data.putBoolean(TAG_POWERED, assembler.isNodePowered());
data.putBoolean(TAG_VIS, assembler.getVisSourceFreq() != 0);
}
}
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
boolean visConnected = accessor.getServerData().getBoolean(TAG_VIS);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
// 要素源状态:已连接(绿)/ 未连接(灰)
tooltip.add(Component.translatable("thaumicenergistics.jade.assembler.vis_source",
Component.translatable(visConnected
? "thaumicenergistics.jade.assembler.vis_connected"
: "thaumicenergistics.jade.assembler.vis_disconnected")
.withStyle(style -> style.withColor(visConnected ? 0xFF55FF55 : 0xFFAAAAAA))));
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "assembler");
}
}
@@ -8,24 +8,31 @@ import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import snownee.jade.api.ui.IElementHelper;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileInfusionMonitor;
import thaumicenergistics.common.tiles.TileInfusionMonitor.RiskData;
/**
* 注魔监视器 Jade 显示:网络连接时展示附近注魔祭坛的 5 档风险 + 不稳定度 + 档位文字。
* 服务端把 BE 缓存的 RiskData 写入 NBT,客户端渲染。
* 韵魔观测器 Jade 显示:
* <ol>
* <li>始终显示 ME 网络状态(在线绿 / 未连接红 / 无能量红 / 无频道橙)。</li>
* <li>仅网络在线时展示附近注魔祭坛的风险数据:5 档风险(纯文字,无星星)+ 不稳定度 + 档位描述。</li>
* </ol>
* 服务端把 BE 缓存的 RiskData 与节点状态写入 NBT,客户端渲染。
*/
public enum InfusionMonitorProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String KEY = "InfusionMonitorRisk";
private static final int TIER_COLOR = 0xFF55FFFF;
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileInfusionMonitor monitor) {
data.putBoolean(TAG_ONLINE, monitor.isNetworkOnline());
data.putBoolean(TAG_POWERED, monitor.isNodePowered());
RiskData risk = monitor.getCachedRisk();
CompoundTag tag = new CompoundTag();
tag.putBoolean("found", risk.found());
@@ -43,13 +50,39 @@ public enum InfusionMonitorProvider implements IBlockComponentProvider, IServerD
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
// 标题:监视器
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.title"));
// 网络状态(始终显示):在线绿 / 无频道橙 / 离线红
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
// 未联网 → 不显示风险数据(风险缓存是每 tick 扫描的,与网络无关,未联网时不应展示)
if (!online) {
return;
}
if (!accessor.getServerData().contains(KEY)) return;
CompoundTag tag = accessor.getServerData().getCompound(KEY);
boolean found = tag.getBoolean("found");
// 标题:监视器 + 状态
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.title"));
if (!found) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.no_matrix"));
return;
@@ -64,9 +97,9 @@ public enum InfusionMonitorProvider implements IBlockComponentProvider, IServerD
String shortages = tag.getString("shortages");
int totalNeeded = tag.getInt("totalNeeded");
// 档位:⭐×tier / 5
String stars = "★".repeat(Math.max(1, tier)) + "☆".repeat(5 - tier);
tooltip.add(Component.literal(stars + " (" + tier + "/5)")
// 风险档位:纯文字(极低/低/中/高/极高),按档位着色
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.tier",
Component.translatable("thaumicenergistics.jade.tier." + tier), tier)
.withStyle(style -> style.withColor(tierColor(tier))));
// 不稳定度(分解:基础 + 祭坛)
@@ -0,0 +1,65 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileProviderBase;
/**
* 注魔供应器 / 源质供应器 Jade 显示:ME 网络连接状态。
* <ul>
* <li>在线:节点 active(有能量 + 有频道)——绿</li>
* <li>无频道:已接线上网但没分到频道——橙</li>
* <li>离线:未接线路 / 网络无能量——红</li>
* </ul>
* 服务端把节点状态写入 NBT,客户端渲染。
*/
public enum InfusionProviderProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileProviderBase provider) {
data.putBoolean(TAG_ONLINE, provider.isActive());
data.putBoolean(TAG_POWERED, provider.getMainNode().isPowered());
}
}
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "provider_network");
}
}
@@ -5,7 +5,11 @@ import snownee.jade.api.IWailaClientRegistration;
import snownee.jade.api.IWailaCommonRegistration;
import snownee.jade.api.IWailaPlugin;
import snownee.jade.api.WailaPlugin;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.common.tiles.TileInfusionMonitor;
import thaumicenergistics.common.tiles.TileInfusionProvider;
import thaumicenergistics.init.ModBlocks;
@WailaPlugin
@@ -14,11 +18,29 @@ public class ThEJadePlugin implements IWailaPlugin {
@Override
public void register(IWailaCommonRegistration registration) {
registration.registerBlockDataProvider(VibrationChamberProvider.INSTANCE, TileEssentiaVibrationChamber.class);
registration.registerBlockDataProvider(InfusionMonitorProvider.INSTANCE, TileInfusionMonitor.class);
// 注魔供应器 + 源质供应器共用网络状态显示
registration.registerBlockDataProvider(InfusionProviderProvider.INSTANCE, TileInfusionProvider.class);
registration.registerBlockDataProvider(InfusionProviderProvider.INSTANCE, TileEssentiaProvider.class);
// 奥术装配器:网络状态 + 要素源状态
registration.registerBlockDataProvider(AssemblerProvider.INSTANCE, TileArcaneAssembler.class);
}
@Override
public void registerClient(IWailaClientRegistration registration) {
Block chamberBlock = ModBlocks.ESSENTIA_VIBRATION_CHAMBER.get();
registration.registerBlockComponent(VibrationChamberProvider.INSTANCE, chamberBlock.getClass());
Block monitorBlock = ModBlocks.INFUSION_MONITOR.get();
registration.registerBlockComponent(InfusionMonitorProvider.INSTANCE, monitorBlock.getClass());
// 注魔供应器 + 源质供应器共用网络状态显示
Block infusionProviderBlock = ModBlocks.INFUSION_PROVIDER.get();
registration.registerBlockComponent(InfusionProviderProvider.INSTANCE, infusionProviderBlock.getClass());
Block essentiaProviderBlock = ModBlocks.ESSENTIA_PROVIDER.get();
registration.registerBlockComponent(InfusionProviderProvider.INSTANCE, essentiaProviderBlock.getClass());
Block assemblerBlock = ModBlocks.ARCANE_ASSEMBLER.get();
registration.registerBlockComponent(AssemblerProvider.INSTANCE, assemblerBlock.getClass());
}
}
@@ -9,7 +9,6 @@ import thaumcraft.api.research.ResearchCategories;
import thaumcraft.api.research.ResearchItem;
import thaumcraft.api.research.ResearchPage;
import thaumcraft.common.research.TCResearchIndex;
import thaumicenergistics.ThaumicEnergistics;
/**
* 伪前置桩节点。在 ThE 的研究 tab 中创建一个占位节点,
@@ -94,11 +93,6 @@ public class PseudoResearchItem extends ResearchItem {
// 保存真实研究引用:内容(名称/文字/页面)通过方法委托到真实研究(TC port 渲染调 getPages())
pseudo.realResearch = realResearch;
ThaumicEnergistics.LOG.debug("[Pseudo] {} ← {}:{} icon={}",
pseudoKey, realCategory, realKey,
pseudo.icon_item != null && !pseudo.icon_item.isEmpty()
? pseudo.icon_item.getDescriptionId() : String.valueOf(pseudo.icon_resource));
return pseudo;
}
@@ -26,42 +26,76 @@ public final class TCReflection {
private TCReflection() {}
/** 从荒古炼金炉内部源质缓存直接抽取(替代蒸馏釜/罐子中转)。 */
/** 读取荒古炼金炉 private aspects 字段(不实现 IAspectContainer;只读,返回活引用用于原地修改)。 */
@Nullable
private static AspectStack extractFromFurnace(BlockEntity be, int maxAmount) {
private static AspectList getFurnaceAspects(BlockEntity be) {
try {
if (furnaceAspectsField == null) {
furnaceAspectsField = be.getClass().getDeclaredField("aspects");
furnaceAspectsField.setAccessible(true);
}
AspectList furnaceAspects = (AspectList) furnaceAspectsField.get(be);
if (furnaceAspects != null && furnaceAspects.size() > 0) {
Aspect[] aspects = furnaceAspects.getAspects();
for (Aspect aspect : aspects) {
if (aspect == null) continue;
int amt = furnaceAspects.getAmount(aspect);
if (amt <= 0) continue;
int toExtract = Math.min(amt, maxAmount);
furnaceAspects.remove(aspect, toExtract);
be.setChanged();
ResourceLocation id = getAspectId(aspect);
return id != null ? new AspectStack(id, toExtract) : null;
}
Object raw = furnaceAspectsField.get(be);
return raw instanceof AspectList list ? list : null;
} catch (Exception e) {
return null;
}
}
/** 荒古炼金炉写回(aspects 为 private 字段,无公开写入 API)。返回实际写入量。 */
private static int addToFurnace(BlockEntity be, Aspect aspect, int amount) {
if (amount <= 0) return 0;
try {
AspectList list = getFurnaceAspects(be);
if (list == null) {
if (furnaceAspectsField == null) return 0;
list = new AspectList();
furnaceAspectsField.set(be, list);
}
} catch (Exception ignored) {
list.add(aspect, amount);
be.setChanged();
return amount;
} catch (Exception e) {
return 0;
}
}
/** 从荒古炼金炉内部源质缓存直接抽取(替代蒸馏釜/罐子中转)。only 非空时只抽指定方面。 */
@Nullable
private static AspectStack extractFromFurnace(BlockEntity be, @Nullable ResourceLocation only, int maxAmount) {
AspectList furnaceAspects = getFurnaceAspects(be);
if (furnaceAspects == null || furnaceAspects.size() <= 0) return null;
for (Aspect aspect : furnaceAspects.getAspects()) {
if (aspect == null) continue;
int amt = furnaceAspects.getAmount(aspect);
if (amt <= 0) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int toExtract = Math.min(amt, maxAmount);
furnaceAspects.remove(aspect, toExtract);
be.setChanged();
return new AspectStack(id, toExtract);
}
return null;
}
@Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, int maxAmount) {
return extractEssentia(be, null, maxAmount);
}
/**
* 从容器抽取源质。only 非空时只抽指定方面(存储总线按 AE 请求的 key 定向抽取)。
*/
@Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, @Nullable ResourceLocation only, int maxAmount) {
if (be == null) {
return null;
}
// ===== 路径0: 荒古炼金炉直连(AlchemicalFurnaceBlockEntity 不实现 IAspectContainer,
// 源质在 private aspects 缓存——直接抽取,替代"必须经蒸馏釜/罐子")=====
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
AspectStack fromFurnace = extractFromFurnace(be, maxAmount);
AspectStack fromFurnace = extractFromFurnace(be, only, maxAmount);
if (fromFurnace != null) return fromFurnace;
}
// ===== 路径0b: TC 管道接口(EssentiaTransport——高级荒古炼金炉喷嘴/源质管道)=====
@@ -70,14 +104,16 @@ public final class TCReflection {
if (!transport.canOutputTo(face)) continue;
Aspect aspect = transport.getEssentiaType(face);
if (aspect == null) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int amount = transport.getEssentiaAmount(face);
if (amount <= 0) continue;
int toExtract = Math.min(amount, maxAmount);
int taken = transport.takeEssentia(aspect, toExtract, face);
if (taken > 0) {
be.setChanged();
ResourceLocation id = getAspectId(aspect);
return id != null ? new AspectStack(id, taken) : null;
return new AspectStack(id, taken);
}
}
}
@@ -94,11 +130,13 @@ public final class TCReflection {
if (aspect == null) continue;
int amt = aspectList.getAmount(aspect);
if (amt <= 0) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int toExtract = Math.min(amt, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged();
ResourceLocation id = getAspectId(aspect);
return id != null ? new AspectStack(id, toExtract) : null;
return new AspectStack(id, toExtract);
}
}
}
@@ -113,11 +151,14 @@ public final class TCReflection {
Aspect aspect = (Aspect) methods[0].invoke(be);
int amount = (int) methods[1].invoke(be);
if (aspect != null && amount > 0) {
ResourceLocation id = getAspectId(aspect);
if (id == null || (only != null && !only.equals(id))) {
return null;
}
int toExtract = Math.min(amount, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged();
ResourceLocation id = getAspectId(aspect);
return id != null ? new AspectStack(id, toExtract) : null;
return new AspectStack(id, toExtract);
}
}
} catch (Exception ignored) {
@@ -128,19 +169,28 @@ public final class TCReflection {
public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return;
if (!(be instanceof IAspectContainer container)) return;
Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return;
// 荒古炼金炉:不实现 IAspectContainer,源质在 private aspects 字段 → 反射写回。
// (此前直接 return 导致导入总线"网络已满"时抽出的源质无处归还而消失)
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
addToFurnace(be, aspect, (int) stack.amount());
return;
}
if (!(be instanceof IAspectContainer container)) return;
container.addToContainer(aspect, (int) stack.amount());
be.setChanged();
}
public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return 0;
if (!(be instanceof IAspectContainer container)) return 0;
Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return 0;
int toInsert = (int) stack.amount();
int toInsert = (int) Math.min(stack.amount(), Integer.MAX_VALUE);
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
return addToFurnace(be, aspect, toInsert);
}
if (!(be instanceof IAspectContainer container)) return 0;
int leftover = container.addToContainer(aspect, toInsert);
int added = toInsert - leftover;
if (added > 0) {
@@ -149,6 +199,61 @@ public final class TCReflection {
return added;
}
/** 非破坏性列出容器内全部源质(存储总线挂载为网络存储、导入总线预检用)。 */
public static java.util.List<AspectStack> listEssentia(@Nullable BlockEntity be) {
java.util.List<AspectStack> result = new java.util.ArrayList<>();
if (be == null) return result;
// 荒古炼金炉(不实现 IAspectContainer)
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
AspectList furnaceAspects = getFurnaceAspects(be);
if (furnaceAspects != null) {
for (Aspect aspect : furnaceAspects.getAspects()) {
if (aspect == null) continue;
int amt = furnaceAspects.getAmount(aspect);
ResourceLocation id = getAspectId(aspect);
if (amt > 0 && id != null) result.add(new AspectStack(id, amt));
}
}
return result;
}
// TC 管道接口
if (be instanceof EssentiaTransport transport) {
for (Direction face : Direction.values()) {
if (!transport.canOutputTo(face)) continue;
Aspect aspect = transport.getEssentiaType(face);
if (aspect == null) continue;
int amount = transport.getEssentiaAmount(face);
ResourceLocation id = getAspectId(aspect);
if (amount > 0 && id != null) result.add(new AspectStack(id, amount));
}
return result;
}
// IAspectContainer(罐子/奥术蒸馏器等)
if (be instanceof IAspectContainer container) {
AspectList list = container.getAspects();
if (list != null) {
for (Aspect aspect : list.getAspects()) {
if (aspect == null) continue;
int amt = list.getAmount(aspect);
ResourceLocation id = getAspectId(aspect);
if (amt > 0 && id != null) result.add(new AspectStack(id, amt));
}
}
}
return result;
}
/** 非破坏性窥视第一个可抽取的源质(导入总线预检:先看容器能给什么,再决定抽多少)。 */
@Nullable
public static AspectStack peekEssentia(@Nullable BlockEntity be, int maxAmount) {
for (AspectStack stack : listEssentia(be)) {
if (stack.isEmpty()) continue;
long amt = Math.min(stack.amount(), Math.max(0, maxAmount));
if (amt > 0) return new AspectStack(stack.aspectId(), amt);
}
return null;
}
@Nullable
public static Aspect findAspectById(ResourceLocation id) {
if (id == null) return null;
@@ -0,0 +1,64 @@
package thaumicenergistics.common.integration.tc;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumcraft.common.items.AspectEssenceItem;
import thaumcraft.common.items.JarBlockItem;
/**
* 源质容器判定:只有要素安瓿与要素罐子才算"源质容器",参与右键存入/装罐。
* <p>
* 不能只判 {@code IEssentiaContainerItem}:TC 里天域之华(wisp_essence)、源质水晶(CRYSTAL)
* 等物品也实现了该接口,但它们不是容器——若不排除,右键会被当源质抽走(例如天域之华
* 会被整个抽干变成空玻璃安瓿)。同一实现类 {@link AspectEssenceItem} 用
* {@link AspectEssenceItem#kind()} 区分形态:
* <ul>
* <li>要素安瓿(变体/自定义要素):{@code AspectEssenceItem$Kind.PHIAL}(固定 8 源质)</li>
* <li>空玻璃安瓿:{@code thaumcraft:glass_phial}(普通 Item,无 Kind)</li>
* <li>要素罐子:{@link JarBlockItem}(TC 罐子统一类型,容量 64)</li>
* <li>天域之华 / 水晶:{@code Kind.WISP / Kind.CRYSTAL} —— 一律排除</li>
* </ul>
*/
public final class ThEEssentiaContainers {
private static final ResourceLocation GLASS_PHIAL_ID =
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial");
private ThEEssentiaContainers() {}
/** 要素安瓿:AspectEssenceItem(Kind.PHIAL),或空玻璃安瓿(glass_phial 无 Kind)。 */
public static boolean isPhial(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
if (stack.getItem() instanceof AspectEssenceItem essence) {
return essence.kind() == AspectEssenceItem.Kind.PHIAL;
}
return stack.getItem() == BuiltInRegistries.ITEM.get(GLASS_PHIAL_ID);
}
/** 要素罐子:TC 罐子统一类型(JarBlockItem)。 */
public static boolean isJar(ItemStack stack) {
return !stack.isEmpty() && stack.getItem() instanceof JarBlockItem;
}
/** 是否源质容器(安瓿或罐子)——右键存入/装罐的唯一判定。 */
public static boolean isEssentiaContainer(ItemStack stack) {
return isPhial(stack) || isJar(stack);
}
/** 容器是否装有源质。空玻璃安瓿恒为 false;天域之华/水晶等非容器恒为 false。 */
public static boolean hasEssentia(ItemStack stack) {
if (!isEssentiaContainer(stack)) {
return false;
}
if (stack.getItem() instanceof IEssentiaContainerItem container) {
var aspects = container.getAspects(stack);
return aspects != null && !aspects.isEmpty();
}
return false;
}
}
@@ -140,8 +140,7 @@ public final class ThEThaumcraft {
// 自动 complete 伪前置节点(不通知玩家)
try {
TCResearchManager.complete(rc.player(), pseudoKey, false);
} catch (Exception e) {
LOG.debug("伪前置自动解锁失败: {} → {}", completedKey, pseudoKey);
} catch (Exception ignored) {
}
});
}
@@ -282,9 +281,15 @@ public final class ThEThaumcraft {
-2, -4, 3, new ItemStack(ModItems.ESSENTIA_PROVIDER_ITEM.get()))
.setRound().setParents("ESSENTIABUSES")
.setPages(
// 源质供应器:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER),
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.2"))
// 源质无线接收器:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.connection.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER_CONNECTION),
// 源质无线绑定工具:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.tool.stage.1"),
new ResearchPage(RecipeRegistration.WIRELESS_CONNECTOR))
.registerResearchItem();
}
@@ -6,6 +6,9 @@ import java.util.Set;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.tooltip.TooltipComponent;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
@@ -21,10 +24,12 @@ import appeng.api.upgrades.UpgradeInventories;
import appeng.items.contents.CellConfig;
import appeng.me.cells.BasicCellInventory;
import appeng.util.ConfigInventory;
import net.minecraft.world.level.Level;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.config.ThEConfig;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.init.ModItems;
import java.util.List;
@@ -207,6 +212,38 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
}
}
//shift+右键 返回 对应等级存储组件+源质存储外壳
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack held = player.getItemInHand(hand);
if (!level.isClientSide) {
if (player.isShiftKeyDown()) {
if (held.getItem() instanceof ItemEssentiaCell cellItem) {
String tier = cellItem.getTier();
Item componentItem = switch (tier) {
case "1k" -> ModItems.STORAGE_COMPONENT_1K.get();
case "4k" -> ModItems.STORAGE_COMPONENT_4K.get();
case "16k" -> ModItems.STORAGE_COMPONENT_16K.get();
case "64k" -> ModItems.STORAGE_COMPONENT_64K.get();
default -> null;
};
if(componentItem != null){
player.addItem(new ItemStack(componentItem,1));
player.addItem(new ItemStack(ModItems.STORAGE_CASING.get(),1));
}
}
}
}
if(player.isShiftKeyDown()){
return InteractionResultHolder.success(ItemStack.EMPTY);
}else{
return InteractionResultHolder.pass(held);
}
}
// ===== 工厂方法 =====
public static ItemEssentiaCell create1k(Item.Properties props) {
return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5);
@@ -0,0 +1,161 @@
package thaumicenergistics.common.items;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaProviderConnection;
import thaumicenergistics.init.ModBlocks;
/**
* 源质无线绑定工具:
* - Shift+右键:绑定/断开(未连接的接收器=选中;供应器=绑定选中的接收器;已连接的接收器=断开)
* - 右键:显示连接信息(聊天栏,坐标可复制)
*/
public class WirelessConnectorItem extends Item {
private static final String KEY_SELECTED_RECEIVER = "SelectedReceiver";
private static final String KEY_SELECTED_PROVIDER = "SelectedProvider";
public WirelessConnectorItem(Properties properties) {
super(properties);
}
private static CompoundTag getSelectionTag(ItemStack stack) {
return stack.getOrDefault(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY).copyTag();
}
private static void setSelectionTag(ItemStack stack, CompoundTag tag) {
stack.set(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.of(tag));
}
private static void clearSelection(ItemStack stack) {
stack.remove(DataComponents.CUSTOM_DATA);
}
private static void showReceiverInfo(Player player, TileEssentiaProviderConnection receiver) {
BlockPos lp = receiver.getLinkedProviderPos();
if (lp != null) {
player.sendSystemMessage(Component.literal("§7[源质无线接收器] 已连接供应器: §a[" + lp.getX() + ", " + lp.getY() + ", " + lp.getZ() + "]"));
} else {
player.sendSystemMessage(Component.literal("§7[源质无线接收器] 未连接任何供应器"));
}
}
private static void showProviderInfo(Player player, TileEssentiaProvider provider) {
var list = provider.getLinkedReceivers();
player.sendSystemMessage(Component.literal("§7[源质供应器] 已连接接收器 (" + list.size() + "/" + TileEssentiaProvider.MAX_LINKED_RECEIVERS + "):"));
if (list.isEmpty()) {
player.sendSystemMessage(Component.literal("§7 (无)"));
} else {
for (BlockPos rp : list) {
player.sendSystemMessage(Component.literal("§a [" + rp.getX() + ", " + rp.getY() + ", " + rp.getZ() + "]"));
}
}
}
@Override
public InteractionResult useOn(UseOnContext context) {
Level level = context.getLevel();
BlockPos pos = context.getClickedPos();
Player player = context.getPlayer();
ItemStack stack = context.getItemInHand();
if (level.isClientSide() || player == null) {
return InteractionResult.SUCCESS;
}
BlockState state = level.getBlockState(pos);
BlockEntity be = level.getBlockEntity(pos);
// ===== Shift+右键:绑定 / 断开(顺序不限,第二步完成绑定) =====
if (player.isShiftKeyDown()) {
if (be instanceof TileEssentiaProviderConnection receiver) {
if (receiver.getLinkedProviderPos() != null) {
// 已连接 → 断开
receiver.clearLink();
player.sendSystemMessage(Component.literal("§e已断开该源质无线接收器的连接"));
return InteractionResult.SUCCESS;
}
CompoundTag tag = getSelectionTag(stack);
if (tag.contains(KEY_SELECTED_PROVIDER)) {
// 已选中供应器 → 绑定到它
BlockPos providerPos = BlockPos.of(tag.getLong(KEY_SELECTED_PROVIDER));
String error = receiver.setLinkedProvider(providerPos);
if (error == null) {
player.sendSystemMessage(Component.literal("§a绑定成功!无线接收器已连接至源质供应器"));
} else {
player.sendSystemMessage(Component.literal(error));
}
clearSelection(stack);
} else if (tag.contains(KEY_SELECTED_RECEIVER)) {
player.sendSystemMessage(Component.literal("§c接收器不能互相绑定,只能绑定到源质供应器"));
} else {
// 未选中 → 选中该接收器
tag.putLong(KEY_SELECTED_RECEIVER, pos.asLong());
setSelectionTag(stack, tag);
player.sendSystemMessage(Component.literal("§a已选中源质无线接收器,请 shift+右键 源质供应器完成绑定"));
}
return InteractionResult.SUCCESS;
}
if (be instanceof TileEssentiaProvider) {
CompoundTag tag = getSelectionTag(stack);
if (tag.contains(KEY_SELECTED_RECEIVER)) {
// 已选中接收器 → 绑定到本供应器
BlockPos receiverPos = BlockPos.of(tag.getLong(KEY_SELECTED_RECEIVER));
if (level.getBlockEntity(receiverPos) instanceof TileEssentiaProviderConnection receiver) {
String error = receiver.setLinkedProvider(pos);
if (error == null) {
player.sendSystemMessage(Component.literal("§a绑定成功!无线接收器已连接至源质供应器"));
} else {
player.sendSystemMessage(Component.literal(error));
}
} else {
player.sendSystemMessage(Component.literal("§c选中的源质无线接收器不存在或已被移除"));
}
clearSelection(stack);
} else {
// 未选中 → 选中该供应器
tag.putLong(KEY_SELECTED_PROVIDER, pos.asLong());
setSelectionTag(stack, tag);
player.sendSystemMessage(Component.literal("§a已选中源质供应器,请 shift+右键 源质无线接收器完成绑定"));
}
return InteractionResult.SUCCESS;
}
// 其他方块:清除选中
if (getSelectionTag(stack).contains(KEY_SELECTED_RECEIVER)
|| getSelectionTag(stack).contains(KEY_SELECTED_PROVIDER)) {
clearSelection(stack);
player.sendSystemMessage(Component.literal("§7已清除选中"));
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
// ===== 右键:显示连接信息(复制显示) =====
if (be instanceof TileEssentiaProviderConnection receiver) {
showReceiverInfo(player, receiver);
return InteractionResult.SUCCESS;
}
if (be instanceof TileEssentiaProvider provider) {
showProviderInfo(player, provider);
return InteractionResult.SUCCESS;
}
// 其他方块:清除选中
if (getSelectionTag(stack).contains(KEY_SELECTED_RECEIVER)
|| getSelectionTag(stack).contains(KEY_SELECTED_PROVIDER)) {
clearSelection(stack);
player.sendSystemMessage(Component.literal("§7已清除选中"));
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
}
@@ -0,0 +1,54 @@
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;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
/**
* 客户端→服务端 网络包:奥术合成终端清空合成网格 / 上报"关闭时自动清理"开关。
* <ul>
* <li>{@link #ACTION_STASH}:立即把网格物品存入 ME 网络(S_STASH 按钮 / 关闭终端时触发),
* 服务端直接执行,不依赖 AE2 quickMove/MOVE_REGION 语义链。</li>
* <li>{@link #ACTION_SET_CLEAR_ON_CLOSE}:上报客户端 AE2 设置"关闭时自动清理合成槽"的开关值,
* 服务端存到菜单实例上,菜单关闭(removed)时据此清理——不依赖 GUI onClose 是否触发。</li>
* </ul>
*/
public record ArcaneTerminalClearC2SPacket(int containerId, int action, boolean flag) implements CustomPacketPayload {
public static final int ACTION_STASH = 0;
public static final int ACTION_SET_CLEAR_ON_CLOSE = 1;
public static final Type<ArcaneTerminalClearC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("arcane_terminal_clear"));
public static final StreamCodec<RegistryFriendlyByteBuf, ArcaneTerminalClearC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, ArcaneTerminalClearC2SPacket::containerId,
ByteBufCodecs.VAR_INT, ArcaneTerminalClearC2SPacket::action,
ByteBufCodecs.BOOL, ArcaneTerminalClearC2SPacket::flag,
ArcaneTerminalClearC2SPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof ContainerArcaneCraftingTerminal menu) {
switch (this.action) {
case ACTION_STASH -> menu.clearGridToNetworkOnServer();
case ACTION_SET_CLEAR_ON_CLOSE -> menu.setClearOnClose(this.flag);
default -> {
}
}
}
});
}
}
@@ -0,0 +1,81 @@
package thaumicenergistics.common.network;
import java.util.List;
import net.minecraft.core.BlockPos;
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.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerArcaneAssembler;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* S2C:奥术装配器配方预览同步(3×3 材料 + 预产物)。
* 合成开始(active=true)发送材料与产物,结束/清空(active=false)。
* 直接发给打开该装配器菜单的玩家,不依赖 BE update tag(与容器数据同步同级可靠)。
*/
public record AssemblerPreviewSyncPacket(
BlockPos pos,
List<ItemStack> grid,
ItemStack result,
boolean active
) implements CustomPacketPayload {
public static final Type<AssemblerPreviewSyncPacket> TYPE =
new Type<>(ThaumicEnergistics.id("assembler_preview_sync"));
public static final StreamCodec<RegistryFriendlyByteBuf, AssemblerPreviewSyncPacket> STREAM_CODEC =
StreamCodec.composite(
BlockPos.STREAM_CODEC, AssemblerPreviewSyncPacket::pos,
ItemStack.OPTIONAL_STREAM_CODEC.apply(ByteBufCodecs.list(9)), AssemblerPreviewSyncPacket::grid,
ItemStack.OPTIONAL_STREAM_CODEC, AssemblerPreviewSyncPacket::result,
ByteBufCodecs.BOOL, AssemblerPreviewSyncPacket::active,
AssemblerPreviewSyncPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnClient(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
var player = ctx.player();
if (player != null && player.containerMenu instanceof ContainerArcaneAssembler menu
&& menu.assembler != null && menu.assembler.getBlockPos().equals(this.pos)) {
menu.applyClientPreview(this.grid, this.result, this.active);
}
});
}
/** 发给打开该装配器菜单的所有玩家。 */
public static void send(TileArcaneAssembler assembler, boolean active) {
if (assembler == null || assembler.getLevel() == null
|| !(assembler.getLevel() instanceof ServerLevel serverLevel)) {
return;
}
// null → EMPTY 兜底:previewGrid 任何时刻都不应含 null(序列化 ItemStack 会 NPE)
ItemStack[] grid = assembler.getPreviewGrid();
List<ItemStack> gridList = new java.util.ArrayList<>(9);
for (int i = 0; i < 9; i++) {
gridList.add(grid != null && grid[i] != null ? grid[i] : ItemStack.EMPTY);
}
ItemStack result = assembler.getCurrentResult();
var packet = new AssemblerPreviewSyncPacket(
assembler.getBlockPos(),
gridList,
result != null ? result : ItemStack.EMPTY,
active);
for (var player : serverLevel.players()) {
if (player.containerMenu instanceof ContainerArcaneAssembler menu && menu.assembler == assembler) {
PacketDistributor.sendToPlayer(player, packet);
}
}
}
}
@@ -70,5 +70,19 @@ public final class ThENetworking {
ResearchGateErrorPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
// ArcaneTerminalClearC2SPacket — C2S(奥术合成终端清空合成网格——S_STASH 按钮 / 关闭时自动清理)
registrar.playToServer(
ArcaneTerminalClearC2SPacket.TYPE,
ArcaneTerminalClearC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// AssemblerPreviewSyncPacket — S2C(奥术装配器配方预览:3×3 材料 + 预产物)
registrar.playToClient(
AssemblerPreviewSyncPacket.TYPE,
AssemblerPreviewSyncPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
}
}
@@ -0,0 +1,134 @@
package thaumicenergistics.common.parts;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.block.entity.BlockEntity;
import appeng.api.config.Actionable;
import appeng.api.networking.security.IActionSource;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.KeyCounter;
import appeng.api.storage.MEStorage;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.common.essentia.AspectContainer;
import thaumcraft.common.essentia.AspectTank;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
* 把相邻源质容器(要素罐子 / 奥术蒸馏器 / 荒古炼金炉 / 源质管道)包装成 {@link MEStorage},
* 供源质存储总线通过 {@code IStorageProvider.mountInventories} 挂载到网络。
* <p>
* 容器内容始终实时读取(不缓存),因此终端、子网、输入/输出总线看到的都是当前存量。
*/
public class EssentiaContainerStorage implements MEStorage {
/** 每次实时解析相邻方块实体(总线朝向方块可被替换/移除)。 */
private final Supplier<BlockEntity> target;
private final Component description;
public EssentiaContainerStorage(Supplier<BlockEntity> target, Component description) {
this.target = target;
this.description = description;
}
@Override
public long insert(AEKey what, long amount, Actionable mode, IActionSource source) {
if (amount <= 0 || !(what instanceof AEssentiaKey key)) {
return 0;
}
BlockEntity be = target.get();
if (be == null) {
return 0;
}
Aspect aspect = TCReflection.findAspectById(key.getId());
if (aspect == null) {
return 0;
}
// TC 内部 AspectContainer(要素罐子 / 奥术蒸馏器 等):单方面储罐,容量可精确读取,
// 因此 SIMULATE 能如实回答"还能装多少"。输入总线的预检依赖这一点:
// 旧实现一律乐观返回 amount,导致容器已满时仍判定"网络装得下",把源质从炉子里抽走。
if (be instanceof AspectContainer aspectContainer) {
AspectTank tank = aspectContainer.aspectTank();
if (tank == null) {
return 0;
}
int free;
if (tank.isEmpty()) {
free = tank.capacity();
} else if (tank.aspect() == aspect) {
free = tank.capacity() - tank.amount();
} else {
free = 0; // 单方面储罐里装的是别的方面
}
if (free <= 0) {
return 0;
}
int request = (int) Math.min(amount, free);
if (mode == Actionable.SIMULATE) {
return request;
}
int accepted = tank.insert(aspect, request);
if (accepted > 0) {
be.setChanged();
}
return accepted;
}
// 其他容器没有"模拟插入"API:SIMULATE 乐观返回,实际插入按容器返回值结算
// (输入总线侧有缓冲兜底,模拟与实插不一致时源质也不会丢)
if (mode == Actionable.SIMULATE) {
return amount;
}
return TCReflection.insertEssentia(be, new AspectStack(key.getId(), amount));
}
@Override
public long extract(AEKey what, long amount, Actionable mode, IActionSource source) {
if (amount <= 0 || !(what instanceof AEssentiaKey key)) {
return 0;
}
BlockEntity be = target.get();
if (be == null) {
return 0;
}
int request = (int) Math.min(amount, Integer.MAX_VALUE);
if (mode == Actionable.SIMULATE) {
// 只统计、不动容器
for (AspectStack stack : TCReflection.listEssentia(be)) {
if (stack.aspectId().equals(key.getId())) {
return Math.min(stack.amount(), request);
}
}
return 0;
}
AspectStack extracted = TCReflection.extractEssentia(be, key.getId(), request);
return extracted == null ? 0 : extracted.amount();
}
@Override
public void getAvailableStacks(KeyCounter out) {
BlockEntity be = target.get();
if (be == null) {
return;
}
List<AspectStack> stacks = TCReflection.listEssentia(be);
for (AspectStack stack : stacks) {
if (stack.amount() <= 0) continue;
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
if (key != null) {
out.add(key, stack.amount());
}
}
}
@Override
public Component getDescription() {
return description;
}
}
@@ -60,8 +60,19 @@ public class EssentiaImportBusPart extends IOBusPart
private static final double IDLE_POWER = 0.5;
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA;
private static final String NBT_BUFFER_ASPECT = "ThEBufferAspect";
private static final String NBT_BUFFER_AMOUNT = "ThEBufferAmount";
private boolean isWorkingFlag = false;
/**
* 待注入 AE 网络的源质缓冲(随总线 NBT 持久化)。
* <p>
* 网络装不下(无源质存储 / 已满 / 模拟与实插不一致)时源质留在这里重试,
* 绝不丢弃;缓冲非空期间也不再从容器抽取,避免"吸走却存不下"。
*/
private AspectStack buffer = AspectStack.EMPTY;
/**
* 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。
@@ -103,37 +114,72 @@ public class EssentiaImportBusPart extends IOBusPart
protected boolean doBusWork(IGrid grid) {
this.isWorkingFlag = false;
// 1) 先冲缓冲:网络有空间时把上次暂存的源质注入
boolean worked = flushBuffer(grid);
Direction side = this.getSide();
if (side == null) return false;
if (side == null) return worked;
BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side);
Level levelRaw = this.getLevel();
if (!(levelRaw instanceof ServerLevel level)) return false;
if (!level.isLoaded(adjacentPos)) return false;
if (!(levelRaw instanceof ServerLevel level)) return worked;
if (!level.isLoaded(adjacentPos)) return worked;
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false;
if (adjacentBE == null) return worked;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, getTransferRate());
if (extracted == null || extracted.isEmpty()) return false;
// 2) 缓冲仍有剩余 = 网络装不下 → 不再从容器抽取(源质留在原容器里)
if (!this.buffer.isEmpty()) return worked;
// 3) 预检 1:先窥视容器能给什么(非破坏性)。无源质直接退出。
int rate = getTransferRate();
AspectStack available = TCReflection.peekEssentia(adjacentBE, rate);
if (available == null || available.isEmpty()) return worked;
AEssentiaKey key = AEssentiaKey.of(available.aspectId());
if (key == null) return worked;
// 4) 预检 2:网络能接收多少。为 0(无源质存储/已满)则完全不抽取。
long acceptable = grid.getStorageService().getInventory().insert(
key, available.amount(), Actionable.SIMULATE, IActionSource.ofMachine(this));
if (acceptable <= 0) return worked;
int toExtract = (int) Math.min(available.amount(), acceptable);
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, available.aspectId(), toExtract);
if (extracted == null || extracted.isEmpty()) return worked;
long injected = this.injectToNetwork(grid, extracted);
if (injected <= 0) {
TCReflection.returnEssentia(adjacentBE, extracted);
return false;
if (injected < extracted.amount()) {
// 模拟与实插不一致(例如挂载的容器存储无法模拟)→ 剩余留在缓冲里重试,绝不归还/丢弃
this.buffer = new AspectStack(extracted.aspectId(), extracted.amount() - injected);
markForSave();
}
double powerCost = injected * TRANSFER_POWER;
grid.getEnergyService().extractAEPower(powerCost, Actionable.MODULATE, PowerMultiplier.CONFIG);
if (injected < extracted.amount()) {
long leftover = extracted.amount() - injected;
TCReflection.returnEssentia(adjacentBE, new AspectStack(extracted.aspectId(), leftover));
}
this.isWorkingFlag = injected > 0 || worked;
return injected > 0 || worked;
}
/** 把缓冲里的源质注入网络。@return 本次是否注入了任意数量 */
private boolean flushBuffer(IGrid grid) {
if (this.buffer.isEmpty()) return false;
long injected = this.injectToNetwork(grid, this.buffer);
if (injected <= 0) return false;
long left = this.buffer.amount() - injected;
this.buffer = left > 0 ? new AspectStack(this.buffer.aspectId(), left) : AspectStack.EMPTY;
markForSave();
this.isWorkingFlag = true;
return true;
}
private void markForSave() {
if (getHost() != null) {
getHost().markForSave();
}
}
private long injectToNetwork(IGrid grid, AspectStack stack) {
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
if (key == null) return 0;
return grid.getStorageService().getInventory().insert(
key, stack.amount(), Actionable.MODULATE,
IActionSource.ofMachine(this));
@@ -143,12 +189,23 @@ public class EssentiaImportBusPart extends IOBusPart
public void readFromNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.readFromNBT(extra, registries);
this.keyTypeSelection.readFromNBT(extra, registries);
this.buffer = AspectStack.EMPTY;
if (extra.contains(NBT_BUFFER_ASPECT) && extra.getLong(NBT_BUFFER_AMOUNT) > 0) {
ResourceLocation id = ResourceLocation.tryParse(extra.getString(NBT_BUFFER_ASPECT));
if (id != null) {
this.buffer = new AspectStack(id, extra.getLong(NBT_BUFFER_AMOUNT));
}
}
}
@Override
public void writeToNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.writeToNBT(extra, registries);
this.keyTypeSelection.writeToNBT(extra);
if (!this.buffer.isEmpty()) {
extra.putString(NBT_BUFFER_ASPECT, this.buffer.aspectId().toString());
extra.putLong(NBT_BUFFER_AMOUNT, this.buffer.amount());
}
}
@Override
@@ -164,3 +221,4 @@ public class EssentiaImportBusPart extends IOBusPart
return this.keyTypeSelection;
}
}
@@ -1,11 +1,14 @@
package thaumicenergistics.common.parts;
import appeng.api.config.Actionable;
import appeng.api.networking.IGrid;
import appeng.api.AECapabilities;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.security.IActionSource;
import appeng.api.parts.IPartCollisionHelper;
import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel;
import appeng.api.storage.IStorageMounts;
import appeng.api.storage.IStorageProvider;
import appeng.api.storage.MEStorage;
import appeng.api.util.KeyTypeSelection;
import appeng.api.util.KeyTypeSelectionHost;
import appeng.core.settings.TickRates;
@@ -16,22 +19,30 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.init.ModMenuTypes;
import java.util.Set;
/**
* 源质存储总线:把相邻的源质容器(要素罐子 / 奥术蒸馏器 / 荒古炼金炉 / 源质管道)
* 或相邻方块自身暴露的 {@code MEStorage}(例如子网接口)挂载为网络存储,
* 使终端与子网能读写该容器。
* <p>
* 实现方式与 AE2 原版存储总线一致({@link IStorageProvider#mountInventories}),
* 不主动搬运源质。旧实现按输入总线逻辑把容器抽干再插入网络、且忽略插入返回值,
* 导致网络塞不下时源质凭空消失——已移除。
*/
public class EssentiaStorageBusPart extends IOBusPart
implements KeyTypeSelectionHost {
implements IStorageProvider, KeyTypeSelectionHost {
@PartModels
public static final ResourceLocation RL_BASE =
@@ -51,9 +62,12 @@ public class EssentiaStorageBusPart extends IOBusPart
public static final PartModel MODEL_ON = new PartModel(RL_BASE, RL_ON);
private static final double IDLE_POWER = 1.0;
private static final int TRANSFER_RATE = 8;
private boolean isWorkingFlag = false;
/** 相邻源质容器的 MEStorage 视图(惰性创建,内部实时解析方块实体)。 */
private EssentiaContainerStorage containerStorage;
private Component storageDescription;
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
selection -> { },
keyType -> keyType == AEssentiaKeyType.INSTANCE);
@@ -61,6 +75,8 @@ public class EssentiaStorageBusPart extends IOBusPart
public EssentiaStorageBusPart(IPartItem<?> partItem) {
super(TickRates.StorageBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
this.getMainNode().setIdlePowerUsage(IDLE_POWER);
// 注册为存储提供者,网络挂载时会调用 mountInventories(与 AE2 原版存储总线一致)
this.getMainNode().addService(IStorageProvider.class, this);
}
@Override
@@ -77,32 +93,81 @@ public class EssentiaStorageBusPart extends IOBusPart
return MODEL_OFF;
}
/** 存储总线不做搬运工作:容器由 {@link #mountInventories} 挂载为网络存储。
* 这里只做状态指示(相邻容器有源质 → 亮工作灯)。 */
@Override
protected boolean doBusWork(IGrid grid) {
this.isWorkingFlag = false;
Direction side = this.getSide();
if (side == null) return false;
BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side);
Level levelRaw = this.getLevel();
if (!(levelRaw instanceof ServerLevel level)) return false;
if (!level.isLoaded(adjacentPos)) return false;
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE);
if (extracted == null || extracted.isEmpty()) return false;
this.insertIntoNetwork(grid, extracted);
this.isWorkingFlag = true;
return true;
protected boolean doBusWork(appeng.api.networking.IGrid grid) {
BlockEntity adjacent = getAdjacentBlockEntity();
this.isWorkingFlag = adjacent != null
&& thaumicenergistics.common.integration.tc.TCReflection.peekEssentia(adjacent, 1) != null;
return false;
}
private void insertIntoNetwork(IGrid grid, AspectStack stack) {
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
grid.getStorageService().getInventory().insert(
key, stack.amount(), Actionable.MODULATE,
IActionSource.ofMachine(this));
// ===== IStorageProvider:把相邻存储挂载到网络 =====
@Override
public void mountInventories(IStorageMounts mounts) {
BlockPos adjacentPos = getAdjacentPos();
if (adjacentPos == null) return;
Level level = getLevel();
if (!(level instanceof ServerLevel serverLevel) || !serverLevel.isLoaded(adjacentPos)) return;
// 1) 相邻方块自身暴露 MEStorage(子网接口等)→ 直接挂载,使终端/子网可访问
Direction side = this.getSide();
MEStorage external = serverLevel.getCapability(AECapabilities.ME_STORAGE, adjacentPos,
side != null ? side.getOpposite() : null);
if (external != null) {
mounts.mount(external, 0);
return;
}
// 2) 否则把相邻源质容器(罐子/蒸馏器/荒古炼金炉/管道)包装成 MEStorage 挂载
mounts.mount(getContainerStorage(), 0);
}
private EssentiaContainerStorage getContainerStorage() {
if (containerStorage == null) {
if (storageDescription == null) {
storageDescription = Component.translatable("item.thaumicenergistics.essentia_storage_bus");
}
containerStorage = new EssentiaContainerStorage(this::getAdjacentBlockEntity, storageDescription);
}
return containerStorage;
}
@Nullable
private BlockPos getAdjacentPos() {
Direction side = this.getSide();
if (side == null || this.getBlockEntity() == null) return null;
return this.getBlockEntity().getBlockPos().relative(side);
}
/** 实时解析相邻方块实体(容器可能被替换/移除,挂载视图每次调用重新读取)。 */
@Nullable
private BlockEntity getAdjacentBlockEntity() {
BlockPos pos = getAdjacentPos();
Level level = getLevel();
if (pos == null || level == null || !level.isLoaded(pos)) return null;
return level.getBlockEntity(pos);
}
@Override
public void addToWorld() {
super.addToWorld();
IStorageProvider.requestUpdate(this.getMainNode());
}
@Override
public void onNeighborChanged(net.minecraft.world.level.BlockGetter level, BlockPos pos, BlockPos neighbor) {
super.onNeighborChanged(level, pos, neighbor);
// 相邻方块变化 → 重新挂载(子网接口 ↔ 源质容器的切换)
IStorageProvider.requestUpdate(this.getMainNode());
}
@Override
protected void onMainNodeStateChanged(IGridNodeListener.State state) {
super.onMainNodeStateChanged(state);
IStorageProvider.requestUpdate(this.getMainNode());
}
@Override
@@ -74,17 +74,26 @@ public class TileArcaneAssembler extends ThETileBase
private static final double ACTIVE_POWER = 1.5;
private static final double WARP_POWER_PERCENT = 0.15;
private static final int MAX_SPEED_UPGRADES = 4;
private static final int VIS_REPLENISH_INTERVAL = 20;
/** vis 充能间隔(tick):每 5 tick 充一次,等效 20 cvis/tick,渐进但够快,避免合成长期卡"等待"。 */
private static final int VIS_REPLENISH_INTERVAL = 5;
/** 每次充能上限(cvis/次,每 VIS_REPLENISH_INTERVAL tick 一次 ≈ 20 cVis/tick)。 */
private static final int MAX_CVIS_PER_REPLENISH = 100;
/** 每次实际 vis 变化累计达到该值才调用一次 setChanged(约 4×5=20 tick),避免合成期间 chunk 持续处于 unsaved 状态。 */
private static final int SET_CHANGED_INTERVAL = 4;
/** 元始珍珠:TC 奥术配方中唯一 retainIngredient 原料(ShapedArcaneRecipe.retainIngredient),合成不消耗。 */
private static final ResourceLocation PRIMORDIAL_PEARL_ID =
ResourceLocation.fromNamespaceAndPath("thaumcraft", "primordial_pearl");
public static final int SLOT_COUNT = 27;
public static final int SLOT_COUNT = 31;
public static final int KCORE_SLOT = 0;
public static final int PATTERN_SLOT_START = 1;
public static final int PATTERN_SLOT_END = 21;
public static final int TARGET_SLOT = 22;
public static final int ARMOR_SLOT_START = 23;
public static final int ARMOR_SLOT_END = 26;
/** 加速卡槽:持久化在 Tile 内(此前放容器局部 handler,关 GUI 即丢失→吞卡)。 */
public static final int UPGRADE_SLOT_START = 27;
public static final int UPGRADE_SLOT_END = 30;
protected final IManagedGridNode mainNode;
private final IActionSource actionSource;
@@ -107,6 +116,15 @@ public class TileArcaneAssembler extends ThETileBase
private boolean isCrafting = false;
private int craftTickCounter = 0;
private int speedUpgrades = 0;
/** 当前合成配方的 3×3 材料网格(GUI 右下配方预览用,随合成开始填充、结束清空)。 */
private ItemStack[] previewGrid = emptyPreviewGrid();
/** 全 EMPTY 的 9 格预览数组(避免 null 元素导致网络包编码崩溃)。 */
private static ItemStack[] emptyPreviewGrid() {
ItemStack[] grid = new ItemStack[9];
java.util.Arrays.fill(grid, ItemStack.EMPTY);
return grid;
}
/**
* 从内存卡粘贴的 vis_interface P2P 频率;装配器优先从该频率的 vis_interface P2P 端抽 vis。
* 0 = 未设置,走 essentia 网络。对应 1.7.10 的 visSourceInfo,但用 P2P 频率替代位置(适配 neo P2P 设计)。
@@ -223,12 +241,11 @@ public class TileArcaneAssembler extends ThETileBase
}
visTickCounter++;
if (visTickCounter >= VIS_REPLENISH_INTERVAL) {
// 配置了 vis 源(内存卡设了 VisInterface 频率)就持续充能:放置时要素条也显示充能过程,
// 合成时直接用缓存,不会卡"等待"。未配置源不充能(要素条空)。
if ((isCrafting || visSourceFreq != 0) && visTickCounter >= VIS_REPLENISH_INTERVAL) {
visTickCounter = 0;
// 仅合成中或 vis 未满时才抽(满时每 5 tick 空转会白跑 P2P/网格查询)
if (isCrafting || !storedVisFull()) {
replenishVis(grid);
}
replenishVis(grid);
}
if (isCrafting) {
@@ -249,46 +266,34 @@ public class TileArcaneAssembler extends ThETileBase
}
private void replenishVis(IGrid grid) {
// 优先从内存卡配置的 vis_interface P2P 源抽 vis(1.7.10 vis 源机制,适配 neo P2P)
if (visSourceFreq != 0) {
appeng.me.service.P2PService p2p = appeng.me.service.P2PService.get(grid);
if (p2p != null) {
appeng.parts.p2p.P2PTunnelPart<?> input = p2p.getInput(visSourceFreq);
if (input instanceof thaumicenergistics.common.parts.VisInterfacePart visSource) {
for (Aspect primal : PRIMALS) {
int needed = MAX_STORED_CVIS - storedVis.getAmount(primal);
if (needed <= 0) continue;
ResourceLocation id = TCReflection.getAspectId(primal);
if (id == null) continue;
int got = visSource.consumeVis(id, needed);
if (got > 0) {
storedVis.add(primal, got);
maybeMarkVisDirty();
}
}
return; // 有 vis 源时只用它(1.7.10 行为:源不可用则不抽)
}
}
// 充能前提:必须先用内存卡配置 vis 源(VisInterface P2P 频率)。
if (visSourceFreq == 0) {
return;
}
appeng.me.service.P2PService p2p = appeng.me.service.P2PService.get(grid);
if (p2p == null) {
return;
}
appeng.parts.p2p.P2PTunnelPart<?> input = p2p.getInput(visSourceFreq);
if (!(input instanceof thaumicenergistics.common.parts.VisInterfacePart visSource)) {
return; // 频率设了但找不到 vis_interface P2P 输入 → 不抽
}
// 无 vis 源:从 essentia 网络抽(neo 默认行为)
IEssentiaGrid essentiaGrid = grid.getService(IEssentiaGrid.class);
if (essentiaGrid == null) return;
// 限速充能。
// 关键单位换算:TC 的 consumeVis/consumeEnergizedVis 返回的是 vis(玩家看到的"X 点要素"),
// 而装配器 storedVis 用 cvis(×CVIS_MULTIPLIER)。返回的 vis 必须 ×CVIS_MULTIPLIER 才等于 cvis,
// 否则合成按 cvis 判定永远不够(之前少了 10 倍导致卡"等待")。
for (Aspect primal : PRIMALS) {
int currentCvis = storedVis.getAmount(primal);
int needed = MAX_STORED_CVIS - currentCvis;
int needed = MAX_STORED_CVIS - storedVis.getAmount(primal);
if (needed <= 0) continue;
// 请求的 vis 数 = 需要补齐的 cvis 换算成 vis,且受每次上限约束
int wantVis = Math.max(1, Math.min(needed / CVIS_MULTIPLIER, MAX_CVIS_PER_REPLENISH / CVIS_MULTIPLIER));
ResourceLocation id = TCReflection.getAspectId(primal);
if (id == null) continue;
int neededEssentia = (needed + CVIS_MULTIPLIER - 1) / CVIS_MULTIPLIER;
long extracted = essentiaGrid.extractEssentia(id, neededEssentia, false);
if (extracted > 0) {
int cvisGained = (int) (extracted * CVIS_MULTIPLIER);
storedVis.add(primal, cvisGained);
int gotVis = visSource.consumeVis(id, wantVis);
if (gotVis > 0) {
storedVis.add(primal, gotVis * CVIS_MULTIPLIER);
maybeMarkVisDirty();
}
}
@@ -308,6 +313,8 @@ public class TileArcaneAssembler extends ThETileBase
private TickRateModulation craftingTick(IGrid grid, int ticksSinceLast) {
if (currentResult.isEmpty()) {
isCrafting = false;
previewGrid = emptyPreviewGrid();
thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, false);
return TickRateModulation.IDLE;
}
@@ -326,24 +333,46 @@ public class TileArcaneAssembler extends ThETileBase
AEItemKey outputKey = AEItemKey.of(output);
if (outputKey == null) return TickRateModulation.IDLE;
// 元始珍珠保留:TC 唯一 retainIngredient 原料,合成后珍珠退回网络(与奥术合成终端一致)。
// CPU 已按配方从网络扣除珍珠,这里补回 → 净零不消耗。
var retainedPearls = retainedPearlStacks();
long inserted = storage.getInventory().insert(
outputKey, output.getCount(), Actionable.SIMULATE, actionSource);
if (inserted < output.getCount()) {
return TickRateModulation.SAME;
}
// 预检:珍珠类型塞满放不回网络则等待,避免合成完成却丢珍珠
for (ItemStack pearl : retainedPearls) {
AEItemKey pearlKey = AEItemKey.of(pearl);
if (pearlKey == null) continue;
if (storage.getInventory().insert(
pearlKey, pearl.getCount(), Actionable.SIMULATE, actionSource) < pearl.getCount()) {
return TickRateModulation.SAME;
}
}
consumeVisForCraft();
storage.getInventory().insert(
outputKey, output.getCount(), Actionable.MODULATE, actionSource);
// 退回珍珠(数量 = 3×3 配方网格中珍珠总数,通常 1 颗)
for (ItemStack pearl : retainedPearls) {
AEItemKey pearlKey = AEItemKey.of(pearl);
if (pearlKey != null) {
storage.getInventory().insert(pearlKey, pearl.getCount(), Actionable.MODULATE, actionSource);
}
}
isCrafting = false;
currentRecipe = null;
currentResult = ItemStack.EMPTY;
currentRecipeAspects = new AspectList();
previewGrid = emptyPreviewGrid();
internalInventory.setItem(TARGET_SLOT, ItemStack.EMPTY);
setChanged();
markForUpdate();
thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, false);
return TickRateModulation.URGENT;
}
@@ -389,6 +418,21 @@ public class TileArcaneAssembler extends ThETileBase
return Math.max(0, Math.round(baseCost * discount));
}
/**
* 配方 3×3 网格中需保留的元始珍珠栈。TC 奥术配方中珍珠是唯一 retainIngredient 原料:
* 在奥术工作台/奥术合成终端中作为"催化"留在合成格不消耗,装配器合成完成后也要退回网络。
*/
private List<ItemStack> retainedPearlStacks() {
List<ItemStack> pearls = new ArrayList<>();
if (previewGrid == null) return pearls;
for (ItemStack stack : previewGrid) {
if (!stack.isEmpty() && stack.getItem() == net.minecraft.core.registries.BuiltInRegistries.ITEM.get(PRIMORDIAL_PEARL_ID)) {
pearls.add(stack.copy());
}
}
return pearls;
}
private int ticksPerCraft() {
return Math.max(4, BASE_TICKS_PER_CRAFT - (4 * speedUpgrades));
}
@@ -471,7 +515,7 @@ public class TileArcaneAssembler extends ThETileBase
}
return new ArcaneAssemblerPattern(AEItemKey.of(result), null, aspects, result, inputs,
acp.getResearch());
acp.getResearch(), acp.getIngredients());
}
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
@@ -485,9 +529,18 @@ public class TileArcaneAssembler extends ThETileBase
this.currentRecipe = arcanePattern.getRecipe();
this.currentRecipeAspects = aspects.copy();
this.currentResult = result.copy();
// 配方预览:复制 3×3 材料网格(不足 9 格补空)
this.previewGrid = emptyPreviewGrid();
ItemStack[] grid = arcanePattern.getGrid();
if (grid != null) {
for (int i = 0; i < 9 && i < grid.length; i++) {
this.previewGrid[i] = grid[i] != null ? grid[i].copy() : ItemStack.EMPTY;
}
}
this.internalInventory.setItem(TARGET_SLOT, result.copy());
setChanged();
markForUpdate();
thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, true);
return true;
}
@@ -523,6 +576,10 @@ public class TileArcaneAssembler extends ThETileBase
public int getSpeedUpgrades() { return speedUpgrades; }
public int getCraftTickCounter() { return craftTickCounter; }
public int getTicksPerCraft() { return ticksPerCraft(); }
/** 当前合成配方的 3×3 材料网格(GUI 右下配方预览;未合成时为空数组)。 */
public ItemStack[] getPreviewGrid() { return previewGrid; }
/** 当前合成产物(GUI 预产物显示;未合成为空)。 */
public ItemStack getCurrentResult() { return currentResult; }
public float getCraftProgress() {
if (!isCrafting || ticksPerCraft() <= 0) return 0f;
return Math.min(1f, (float) craftTickCounter / ticksPerCraft());
@@ -538,6 +595,8 @@ public class TileArcaneAssembler extends ThETileBase
}
public short getVisSourceFreq() { return visSourceFreq; }
/** ME 节点是否有能量(Jade 网络状态细分用)。 */
public boolean isNodePowered() { return mainNode != null && mainNode.isPowered(); }
/**
* 内存卡右键装配器:读 P2P 频率,设为 vis 源。
@@ -582,12 +641,24 @@ public class TileArcaneAssembler extends ThETileBase
if (slot >= ARMOR_SLOT_START && slot <= ARMOR_SLOT_END) {
calculateVisDiscounts();
}
if (slot >= UPGRADE_SLOT_START && slot <= UPGRADE_SLOT_END) {
recountSpeedUpgrades();
}
if (slot >= PATTERN_SLOT_START && slot <= PATTERN_SLOT_END) {
stalePatterns = true;
}
setChanged();
}
/** 按 Tile 内加速卡槽的实际卡片数量刷新 speedUpgrades(持久化计数,不再依赖菜单存活)。 */
private void recountSpeedUpgrades() {
int count = 0;
for (int i = UPGRADE_SLOT_START; i <= UPGRADE_SLOT_END; i++) {
if (!internalInventory.getItem(i).isEmpty()) count++;
}
setSpeedUpgrades(count);
}
private void updatePatternSlots() {
ItemStack kCore = internalInventory.getItem(KCORE_SLOT);
if (kCore.isEmpty() || !kCore.is(ModItems.KNOWLEDGE_CORE.get())) {
@@ -628,6 +699,15 @@ public class TileArcaneAssembler extends ThETileBase
tag.put("Slot" + i, stack.save(reg));
}
}
// 配方预览 3×3 材料(GUI 右下角显示)
CompoundTag gridTag = new CompoundTag();
for (int i = 0; i < 9; i++) {
ItemStack stack = previewGrid[i];
if (stack != null && !stack.isEmpty()) {
gridTag.put("g" + i, stack.save(reg));
}
}
tag.put("PreviewGrid", gridTag);
return tag;
}
@@ -644,6 +724,15 @@ public class TileArcaneAssembler extends ThETileBase
internalInventory.setItem(i, ItemStack.EMPTY);
}
}
previewGrid = emptyPreviewGrid();
if (tag.contains("PreviewGrid")) {
CompoundTag gridTag = tag.getCompound("PreviewGrid");
for (int i = 0; i < 9; i++) {
if (gridTag.contains("g" + i)) {
previewGrid[i] = ItemStack.parseOptional(reg, gridTag.getCompound("g" + i));
}
}
}
} finally {
loadingFromClientTag = false;
}
@@ -684,6 +773,7 @@ public class TileArcaneAssembler extends ThETileBase
updatePatternSlots();
stalePatterns = true;
calculateVisDiscounts();
recountSpeedUpgrades();
}
@Override
@@ -720,21 +810,30 @@ public class TileArcaneAssembler extends ThETileBase
private final ItemStack resultStack;
private final IInput[] inputs;
private final String research;
/** 3×3 配方网格(知识核心存储的 9 格材料,GUI 配方预览用)。 */
private final ItemStack[] grid;
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks) {
this(definition, recipe, aspects, resultStack, inputStacks, null);
this(definition, recipe, aspects, resultStack, inputStacks, null, null);
}
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks, String research) {
this(definition, recipe, aspects, resultStack, inputStacks, research, null);
}
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks, String research, ItemStack[] grid) {
this.definition = definition;
this.recipe = recipe;
this.aspects = aspects;
this.resultStack = resultStack;
this.research = research;
this.grid = grid;
this.inputs = new IInput[inputStacks.size()];
for (int i = 0; i < inputStacks.size(); i++) {
GenericStack stack = inputStacks.get(i);
@@ -759,6 +858,13 @@ public class TileArcaneAssembler extends ThETileBase
@Nullable
@Override
public AEKey getRemainingKey(AEKey what) {
// 元始珍珠作为"催化"原料:声明合成后它会返回网络(AE2 原生容器机制)。
// 规划端据此按"每次 1 单"顺序模拟、执行端把珍珠计入 CPU waitingFor,
// 机器合成完成把珍珠退回网络后 CPU 自动捕获续下一单——1 颗珍珠可连续多单。
if (what instanceof AEItemKey itemKey
&& itemKey.getItem() == net.minecraft.core.registries.BuiltInRegistries.ITEM.get(PRIMORDIAL_PEARL_ID)) {
return what;
}
return null;
}
};
@@ -796,6 +902,9 @@ public class TileArcaneAssembler extends ThETileBase
public IArcaneRecipe getRecipe() { return recipe; }
public AspectList getAspects() { return aspects; }
public ItemStack getResultStack() { return resultStack; }
/** 3×3 配方网格(可能为 null/短数组,GUI 预览用)。 */
@Nullable
public ItemStack[] getGrid() { return grid; }
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@Nullable
public String getResearch() { return research; }
@@ -8,7 +8,6 @@ import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.IEssentiaTransport;
import thaumcraft.common.essentia.EssentiaTransport;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.IAspectStack;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModItems;
@@ -33,7 +32,64 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override
protected double getIdlePowerUsage() {
return 3.0;
// 基础 3 + 每连接 1 个接收器 +5
return 3.0 + 5.0 * getLinkedReceiverCount();
}
// ===== 无线接收器连接管理 =====
/** 最多连接的接收器数量。 */
public static final int MAX_LINKED_RECEIVERS = 8;
/** 最大连接距离(格)。 */
public static final int MAX_LINK_DISTANCE = 32;
private List<BlockPos> linkedReceivers = new java.util.ArrayList<>();
public java.util.List<BlockPos> getLinkedReceivers() {
return linkedReceivers;
}
public int getLinkedReceiverCount() {
return linkedReceivers.size();
}
/** 注册一个接收器连接(校验数量与距离,失败返回原因文案;成功返回 null)。 */
public String addLinkedReceiver(BlockPos receiverPos) {
if (linkedReceivers.contains(receiverPos)) return null; // 已连接
if (linkedReceivers.size() >= MAX_LINKED_RECEIVERS) {
return "§c该源质供应器已连接满 " + MAX_LINKED_RECEIVERS + " 个接收器";
}
double dist = Math.sqrt(worldPosition.distSqr(receiverPos));
if (dist > MAX_LINK_DISTANCE) {
return "§c距离过远(" + (int) Math.ceil(dist) + " 格 > " + MAX_LINK_DISTANCE + " 格上限)";
}
linkedReceivers.add(receiverPos);
setChanged();
updatePower();
return null;
}
public void removeLinkedReceiver(BlockPos receiverPos) {
if (linkedReceivers.remove(receiverPos)) {
setChanged();
updatePower();
}
}
/** 周期校验:清除已失效的接收器连接(方块被拆除)。 */
public void validateLinkedReceivers() {
if (level == null || linkedReceivers.isEmpty()) return;
linkedReceivers.removeIf(pos -> {
boolean gone = !(level.getBlockEntity(pos) instanceof TileEssentiaProviderConnection);
if (gone) setChanged();
return gone;
});
updatePower();
}
private void updatePower() {
if (mainNode != null) {
mainNode.setIdlePowerUsage(getIdlePowerUsage());
}
}
/** getEssentiaList 短时缓存:TC 管道 suction 每 tick 调 getEssentiaType,避免每 tick 全量遍历。 */
@@ -55,6 +111,12 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
if (essentiaListCacheTimer > 0) essentiaListCacheTimer--;
// 周期校验无线接收器连接(约每 2 秒)
if (++validateCounter >= 40) {
validateCounter = 0;
validateLinkedReceivers();
}
if (++tickCount >= tickRate) {
tickCount = 0;
tickRate = TICK_RATE_IDLE;
@@ -62,6 +124,8 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
}
}
private int validateCounter = 0;
// ===== IEssentiaTransport =====
@Override
@@ -95,11 +159,8 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override
public int takeEssentia(Aspect aspect, int amount, Direction side) {
ThaumicEnergistics.LOG.debug("[MyHano-E] takeEssentia: aspect={}, amount={}, side={}, active={}",
aspect != null ? aspect.getTag() : "null", amount, side, mainNode.isActive());
if (!mainNode.isActive()) return 0;
long extracted = extractEssentiaFromNetwork(aspect, amount, false);
ThaumicEnergistics.LOG.debug("[MyHano-E] takeEssentia result={}", extracted);
if (extracted > 0) {
tickRate = TICK_RATE_URGENT;
}
@@ -108,13 +169,10 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override
public int addEssentia(Aspect aspect, int amount, Direction side) {
ThaumicEnergistics.LOG.debug("[MyHano-E] addEssentia: aspect={}, amount={}, side={}, active={}",
aspect != null ? aspect.getTag() : "null", amount, side, mainNode.isActive());
if (!mainNode.isActive()) return 0;
long rejected = injectEssentiaToNetwork(aspect, amount, false);
long accepted = amount - rejected;
if (accepted > 0) {
ThaumicEnergistics.LOG.debug("[MyHano-E] addEssentia accepted={}", accepted);
tickRate = TICK_RATE_URGENT;
}
return (int) accepted;
@@ -151,6 +209,36 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
return 1;
}
// ===== 供源质无线接收器转发的公开方法 =====
public boolean thE_isActive() {
return mainNode.isActive();
}
public long thE_queryAmount(Aspect aspect) {
return getAspectAmountInNetwork(aspect);
}
public long thE_extract(Aspect aspect, long amount, boolean simulate) {
return extractEssentiaFromNetwork(aspect, amount, simulate);
}
public long thE_inject(Aspect aspect, long amount, boolean simulate) {
return injectEssentiaToNetwork(aspect, amount, simulate);
}
/** 网络中第一种有存量的源质(供接收器无特定需求时兜底)。 */
public Aspect thE_firstAvailable() {
if (essentiaGrid != null) {
for (var stack : getCachedEssentiaList()) {
if (stack.amount() > 0) {
return Aspect.getAspect(stack.aspectId().getPath());
}
}
}
return null;
}
@Override
public boolean renderExtendedTube() {
return false;
@@ -199,4 +287,27 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
}
}
}
// ===== NBT =====
@Override
protected void saveAdditional(net.minecraft.nbt.CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag();
for (BlockPos pos : linkedReceivers) {
list.add(net.minecraft.nbt.LongTag.valueOf(pos.asLong()));
}
tag.put("LinkedReceivers", list);
}
@Override
protected void loadAdditional(net.minecraft.nbt.CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
linkedReceivers.clear();
if (tag.contains("LinkedReceivers")) {
for (net.minecraft.nbt.Tag t : tag.getList("LinkedReceivers", net.minecraft.nbt.Tag.TAG_LONG)) {
linkedReceivers.add(BlockPos.of(((net.minecraft.nbt.LongTag) t).getAsLong()));
}
}
}
}
@@ -0,0 +1,326 @@
package thaumicenergistics.common.tiles;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaTransport;
import thaumcraft.common.essentia.EssentiaTransport;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModBlocks;
/**
* 源质无线接收器:通过无线连接器与源质供应器建立连接后,
* 作为纯中继(无 AE 节点),把 TC 管道/炼金塔的取源质请求转发到连接的供应器。
*/
public class TileEssentiaProviderConnection extends BlockEntity implements IEssentiaTransport, EssentiaTransport {
/** 连接的源质供应器坐标(null = 未连接)。 */
private BlockPos linkedProviderPos;
/** 接收器每次输入/输出源质上限。 */
public static final int TRANSFER_LIMIT = 16;
/**
* 待注入 AE 网络的源质缓冲。
* <p>
* 网络装不下的源质一律留在这里(随 NBT 持久化),等网络有空间再注入——
* 旧实现直接 {@code provider.thE_inject(...)} 且忽略返回值,
* 而供应器侧一旦把源质写进内存 monitor 就"全部接受",网络无存储时源质凭空消失。
*/
private final AspectList buffer = new AspectList();
private static final String NBT_BUFFER = "ThEBuffer";
private int linkCheckCounter = 0;
private int pullCounter = 0;
private static final int PULL_INTERVAL = 10;
/** 模拟管道的最大扫描距离(格):接收器可隔空拉取该距离内的源质源。 */
private static final int PIPE_SCAN_RANGE = 16;
/** 服务端 tick:周期校验连接 + 主动沿模拟管道拉取源质(蒸馏器/罐/管道不主动推,需接收器拉)。 */
public static void serverTick(net.minecraft.world.level.Level level, BlockPos pos, BlockState state, TileEssentiaProviderConnection be) {
if (level.isClientSide()) return;
if (be.linkedProviderPos != null && ++be.linkCheckCounter >= 40) {
be.linkCheckCounter = 0;
be.getLinkedProvider(); // 无效连接会被清除
}
if (++be.pullCounter >= PULL_INTERVAL) {
be.pullCounter = 0;
be.flushBuffer(); // 先把上次暂存的源质注入网络
be.takeEssentiaFromNeighbors(); // 缓冲清空(网络有余量)时才继续拉取
}
}
/**
* 把缓冲里的源质注入连接的网络。网络满时保留剩余部分,下次再试。
* @return 本次是否有源质被网络接收
*/
private boolean flushBuffer() {
if (buffer.isEmpty()) return false;
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return false;
boolean moved = false;
for (Aspect aspect : buffer.getAspects()) {
if (aspect == null) continue;
int amt = buffer.getAmount(aspect);
if (amt <= 0) continue;
long rejected = provider.thE_inject(aspect, amt, false);
int accepted = amt - (int) rejected;
if (accepted > 0) {
int left = amt - accepted;
if (left > 0) {
buffer.set(aspect, left);
} else {
buffer.aspects.remove(aspect);
}
moved = true;
}
if (rejected > 0) break; // 网络已满:剩余留在缓冲里
}
if (moved) setChanged();
return moved;
}
/** 模拟管道:沿 6 个方向扫描,最近的有效源质源(蒸馏器/罐/管道)隔空拉取,经连接供应器注入 AE 网络。 */
private void takeEssentiaFromNeighbors() {
if (level == null || level.isClientSide()) return;
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return;
// 缓冲还没清空 = 网络装不下,先别再吸(否则"吸走却存不下")
if (!buffer.isEmpty()) return;
for (Direction side : Direction.values()) {
if (!canInputFrom(side)) continue;
// 沿方向行走:非传输方块 = 管道断;空管道 = 继续;有源质 = 拉取
for (int d = 1; d <= PIPE_SCAN_RANGE; d++) {
BlockPos sourcePos = worldPosition.relative(side, d);
if (!level.isLoaded(sourcePos)) break;
BlockEntity sourceTile = level.getBlockEntity(sourcePos);
if (sourceTile == this) break;
if (sourceTile instanceof TileEssentiaProvider) break; // 到供应器本体,防循环
if (!(sourceTile instanceof IEssentiaTransport source)) break; // 非传输方块 = 管道断
Direction sourceSide = side.getOpposite();
if (!source.canOutputTo(sourceSide)) break; // 不能向接收器方向输出 = 断
int srcAmount = source.getEssentiaAmount(sourceSide);
if (srcAmount <= 0) continue; // 空管道/空源 → 继续走
int dSuck = getSuctionAmount(side);
if (dSuck < source.getMinimumSuction() || dSuck < source.getSuctionAmount(sourceSide)) {
break;
}
Aspect sourceAspect = source.getEssentiaType(sourceSide);
if (sourceAspect == null) break;
// 预检:网络能收多少(rejected = 收不下的量);一点都收不下就完全不抽取
int want = Math.min(Math.max(srcAmount, 1), TRANSFER_LIMIT);
long rejected = provider.thE_inject(sourceAspect, want, true);
int room = want - (int) rejected;
if (room <= 0) break;
int taken = source.takeEssentia(sourceAspect, room, sourceSide);
if (taken > 0) {
buffer.add(sourceAspect, taken); // 先进缓冲,注入失败也不丢
flushBuffer();
}
break; // 拉到即停
}
}
}
public TileEssentiaProviderConnection(BlockPos pos, BlockState state) {
super(ModBlockEntities.ESSENTIA_PROVIDER_CONNECTION.get(), pos, state);
}
// ===== 连接管理 =====
public BlockPos getLinkedProviderPos() {
return linkedProviderPos;
}
public boolean isLinked() {
return linkedProviderPos != null && getLinkedProvider() != null;
}
/** 设置连接(无线连接器右键供应器时调用)。返回 null = 成功;否则返回失败原因文案。 */
public String setLinkedProvider(BlockPos providerPos) {
// 在供应器侧注册(校验数量/距离)
if (level != null && !level.isClientSide()) {
if (level.getBlockEntity(providerPos) instanceof TileEssentiaProvider provider) {
String error = provider.addLinkedReceiver(worldPosition);
if (error != null) {
return error; // 超限/超距,不建立连接
}
} else {
return "§c目标不是有效的源质供应器";
}
}
this.linkedProviderPos = providerPos;
setChanged();
updateConnectedState();
return null;
}
public void clearLink() {
// 从供应器侧注销
if (level != null && !level.isClientSide() && linkedProviderPos != null
&& level.getBlockEntity(linkedProviderPos) instanceof TileEssentiaProvider provider) {
provider.removeLinkedReceiver(worldPosition);
}
this.linkedProviderPos = null;
setChanged();
updateConnectedState();
}
private void updateConnectedState() {
if (level == null || level.isClientSide()) return;
boolean connected = linkedProviderPos != null;
BlockState st = getBlockState();
if (st.getValue(ModBlocks.CONNECTED) != connected) {
level.setBlock(worldPosition, st.setValue(ModBlocks.CONNECTED, connected), 3);
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
/** 查找连接的供应器 tile。 */
public TileEssentiaProvider getLinkedProvider() {
if (level == null || level.isClientSide() || linkedProviderPos == null) return null;
if (level.getBlockEntity(linkedProviderPos) instanceof TileEssentiaProvider provider) {
// 自愈:供应器列表缺本接收器时补注册(修复旧存档/列表丢失导致的状态不一致)
if (!provider.getLinkedReceivers().contains(worldPosition)) {
provider.addLinkedReceiver(worldPosition);
}
return provider;
}
// 供应器被移除 → 清除连接
linkedProviderPos = null;
setChanged();
updateConnectedState();
return null;
}
// ===== IEssentiaTransport(转发到连接供应器) =====
@Override
public boolean isConnectable(Direction side) {
return true;
}
@Override
public boolean canInputFrom(Direction side) {
return true; // 支持输入源质到 AE 网络
}
@Override
public boolean canOutputTo(Direction side) {
return true;
}
@Override
public void setSuction(Aspect aspect, int amount) {
}
@Override
public Aspect getSuctionType(Direction side) {
return null;
}
@Override
public int getSuctionAmount(Direction side) {
// 与源质供应器一致:有吸力才能被蒸馏器等设施注入源质(输入功能)
return 8;
}
@Override
public int takeEssentia(Aspect aspect, int amount, Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0;
return (int) provider.thE_extract(aspect, Math.min(amount, TRANSFER_LIMIT), false);
}
@Override
public int addEssentia(Aspect aspect, int amount, Direction side) {
// 无线输入:先问网络能收多少,只接收装得下的部分(其余留在管道/容器里,绝不吞掉)。
// 接收后先进缓冲,注入失败下次重试——保证源质不丢。
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0;
int want = Math.min(amount, TRANSFER_LIMIT);
long rejected = provider.thE_inject(aspect, want, true);
int room = want - (int) rejected;
if (room <= 0) return 0; // 网络无源质存储或已满 → 拒收
buffer.add(aspect, room);
flushBuffer();
setChanged();
return room;
}
@Override
public int getEssentiaAmount(Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return 0;
Aspect wanted = getNeighborWantedAspect(side);
if (wanted != null) {
return (int) Math.min(provider.thE_queryAmount(wanted), Integer.MAX_VALUE);
}
return 0;
}
@Override
public Aspect getEssentiaType(Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return null;
Aspect wanted = getNeighborWantedAspect(side);
if (wanted != null && provider.thE_queryAmount(wanted) > 0) {
return wanted;
}
return provider.thE_firstAvailable();
}
@Override
public int getMinimumSuction() {
return 0;
}
@Override
public boolean renderExtendedTube() {
return false;
}
private Aspect getNeighborWantedAspect(Direction side) {
BlockPos neighborPos = worldPosition.relative(side);
if (level.getBlockEntity(neighborPos) instanceof IEssentiaTransport transport) {
return transport.getSuctionType(side.getOpposite());
}
return null;
}
// ===== NBT =====
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
if (linkedProviderPos != null) {
tag.putLong("LinkedProvider", linkedProviderPos.asLong());
}
if (!buffer.isEmpty()) {
buffer.writeToNBT(tag, NBT_BUFFER);
}
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
if (tag.contains("LinkedProvider")) {
linkedProviderPos = BlockPos.of(tag.getLong("LinkedProvider"));
} else {
linkedProviderPos = null;
}
buffer.aspects.clear();
if (tag.contains(NBT_BUFFER)) {
buffer.readFromNBT(tag, NBT_BUFFER);
}
}
}
@@ -54,7 +54,11 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
/** 手册槽位。 */
public static final int BOOK_SLOT = 0;
private static final int SCAN_INTERVAL = 10; // 每 10 tick 扫描一次祭坛
private static final int SCAN_INTERVAL = 10; // 每 10 tick 验证一次祭坛(缓存命中时仅 1 次查询)
/** 未找到祭坛时的扫描退避间隔(tick):全量扫描约 9800 次 getBlockEntity,附近无祭坛时降低频率控 mspt。 */
private static final int SCAN_MISS_INTERVAL = 200;
/** 风险重算限频(tick):computeRisk 内部也要扫上千次 getBlockEntity(配方估算/源质汇总),控制 mspt。 */
private static final int RISK_INTERVAL = 40;
protected final IManagedGridNode mainNode;
private final SimpleContainer inventory = new SimpleContainer(1) {
@@ -67,7 +71,16 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
};
private int scanCounter = 0;
private int riskCounter = 0;
private BlockPos matrixPos = null;
/** 上次扫描到的祭坛注魔中状态(翻转时强制立即重算风险,抓住注魔启动瞬间的高不稳定度)。 */
private boolean cachedCrafting = false;
/** 区块卸载/方块移除中:禁止 setBlock / markForUpdate(卸载时改方块状态会触发区块重载,停服保存死循环)。 */
private boolean unloading = false;
/** 上一次写入气泡的文本(内容未变则跳过反射/NBT 重建,避免每 tick 开销)。 */
private net.minecraft.network.chat.Component lastBubbleText = null;
/** 缓存的注魔风险数据(弹窗显示)。 */
private RiskData cachedRisk = RiskData.NONE;
@@ -112,6 +125,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override
public void onLoad() {
super.onLoad();
unloading = false;
if (level != null && !level.isClientSide()) {
mainNode.create(level, getBlockPos());
}
@@ -121,6 +135,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override
public void setRemoved() {
unloading = true; // 先置标志再销毁网格节点:节点销毁回调会触发 updateNetworkState
super.setRemoved();
if (level != null && !level.isClientSide()) {
discardBubble();
@@ -132,6 +147,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override
public void onChunkUnloaded() {
unloading = true; // 同上:卸载期间禁止改方块状态,否则 chunk 被重新标记脏 → 保存循环
super.onChunkUnloaded();
if (mainNode != null) {
mainNode.destroy();
@@ -151,7 +167,10 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) {
if (level.isClientSide()) return;
if (++be.scanCounter >= SCAN_INTERVAL) {
// 无祭坛缓存时全量扫描退避到 SCAN_MISS_INTERVAL(否则每 10 tick 扫 9800 次 getBlockEntity,
// 附近没建祭坛时也持续 ~70us/t)
int interval = be.matrixPos == null ? SCAN_MISS_INTERVAL : SCAN_INTERVAL;
if (++be.scanCounter >= interval) {
be.scanCounter = 0;
be.scanMatrix();
}
@@ -159,16 +178,50 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
be.updateBubble();
}
/** 扫描附近注魔矩阵并缓存风险数据。 */
/** 扫描附近注魔矩阵并缓存风险数据。
* 快速路径:已缓存矩阵仍在 → 只做 1 次 getBlockEntity 验证;
* 全量扫描(33×9×33≈9800 次 getBlockEntity)仅缓存失效时执行,是 mspt 大户。
* 风险重算限频(配方估算/源质汇总还要上千次查询),但注魔开始/结束瞬间(crafting 翻转)强制立即重算。 */
private void scanMatrix() {
BlockPos found = null;
InfusionRiskCalculator.InfusionMatrixInfo info = InfusionRiskCalculator.scanNearbyMatrix(level, worldPosition);
RiskData newRisk = info != null ? InfusionRiskCalculator.computeRisk(level, info) : RiskData.NONE;
BlockPos found = matrixPos;
boolean craftingChanged = false;
// 快速路径:缓存矩阵仍在 → 跳过全量扫描
if (found != null) {
if (level.getBlockEntity(found) instanceof thaumcraft.common.blockentities.InfusionMatrixBlockEntity matrix) {
boolean crafting = matrix.crafting();
craftingChanged = crafting != cachedCrafting;
cachedCrafting = crafting;
} else {
found = null; // 缓存失效 → 走全量扫描
}
}
if (found == null) {
InfusionRiskCalculator.InfusionMatrixInfo info = InfusionRiskCalculator.scanNearbyMatrix(level, worldPosition);
found = info != null ? info.matrixPos() : null;
}
RiskData newRisk = cachedRisk;
if (found != null) {
// 按 tick 累计(每轮扫描间隔 SCAN_INTERVAL);首次发现即时重算(覆盖层/气泡 0.5 秒内出现),
// 之后每 RISK_INTERVAL tick 重算一次(按"扫描轮数"计数会变成 40 轮×10 tick=20 秒才出数据);
// 注魔开始/结束瞬间(crafting 翻转)也立即重算,抓住启动时的高不稳定度。
riskCounter += SCAN_INTERVAL;
if (riskCounter >= RISK_INTERVAL || !cachedRisk.found() || craftingChanged) {
riskCounter = 0;
if (level.getBlockEntity(found) instanceof thaumcraft.common.blockentities.InfusionMatrixBlockEntity matrix) {
newRisk = InfusionRiskCalculator.computeRisk(level,
new InfusionRiskCalculator.InfusionMatrixInfo(found, matrix));
} else {
newRisk = RiskData.NONE;
}
}
} else {
newRisk = RiskData.NONE;
cachedCrafting = false;
}
boolean riskChanged = !newRisk.equals(this.cachedRisk);
this.cachedRisk = newRisk;
if (info != null) {
found = info.matrixPos();
}
if (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) {
this.matrixPos = found;
markForUpdate();
@@ -178,7 +231,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
// ===== 网格节点 / 状态同步 =====
private void updateNetworkState() {
if (level == null || level.isClientSide()) return;
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean net = mainNode.isActive();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.NETWORK) != net) {
@@ -192,7 +245,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
}
private void updateBookState() {
if (level == null || level.isClientSide()) return;
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.BOOK) != hasBook) {
@@ -211,17 +264,23 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
* 满足 → 生成/更新气泡;不满足 → 销毁气泡。
*/
private void updateBubble() {
if (level == null || level.isClientSide()) return;
if (level == null || level.isClientSide() || unloading) return;
boolean shouldShow = getBlockState().getValue(ModBlocks.BOOK)
&& getBlockState().getValue(ModBlocks.NETWORK)
&& cachedRisk.found();
if (shouldShow) {
ensureBubble();
if (bubble != null) {
applyBubbleNbt(buildBubbleText(cachedRisk));
// 文本未变则跳过反射/NBT 重建(原实现每 tick 都重建,是 mspt 开销之一)
net.minecraft.network.chat.Component text = buildBubbleText(cachedRisk);
if (!text.equals(lastBubbleText)) {
lastBubbleText = text;
applyBubbleNbt(text);
}
}
} else {
discardBubble();
lastBubbleText = null;
}
}
@@ -232,7 +291,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
if (bubble != null) return;
bubble = EntityType.TEXT_DISPLAY.create(level);
if (bubble == null) return;
bubble.setPos(worldPosition.getX() + 0.5, worldPosition.getY() + 1.4, worldPosition.getZ() + 0.5);
bubble.setPos(worldPosition.getX() + 0.5, worldPosition.getY() + 1.2, worldPosition.getZ() + 0.5);
// 1.21.1 TextDisplay 配置 API 均为 private/protected:反射调用 readAdditionalSaveData
// 用 summon 命令同款 NBT 一次配置全部字段(text/background/billboard/shadow/see_through 等)。
applyBubbleNbt(buildBubbleText(cachedRisk));
@@ -259,6 +318,9 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
nbt.putInt("line_width", BUBBLE_LINE_WIDTH);
nbt.putBoolean("shadow", true);
nbt.putBoolean("see_through", false);
// 必须写 alignment:TextDisplay.readAdditionalSaveData 无条件 decode "alignment",
// 缺失时 tag.get 返回 null → Align.CODEC(字符串) decode 失败 → 服务端疯狂刷 "Display entityNot a string"。
nbt.putString("alignment", "center");
// 文字调小:transformation.scale 整体缩到 0.5(text_display 无字号参数,靠实体缩放)
CompoundTag transform = new CompoundTag();
transform.put("scale", floatList(0.5F, 0.5F, 0.5F));
@@ -304,7 +366,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
bubble = null;
}
/** 风险数据 → 气泡多行文本(2 行:注魔风险/不稳定度;描述改由右键在聊天框显示)。 */
/** 风险数据 → 气泡多行文本(2 行:注魔风险/不稳定度;不显示基础+祭坛细分,细分在聊天栏)。 */
private static Component buildBubbleText(RiskData risk) {
// 内边距必须用不间断空格(\u00A0):普通空格在行尾会被字体布局裁剪,
// 导致背景只包住左侧、右侧贴边。NBSP 有真实字形,两侧都保留。
@@ -313,8 +375,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
Component line1 = Component.literal(pad + "注魔风险: " + tierWord + pad)
.withStyle(style -> style.withColor(tierColor(risk.tier())));
Component line2 = Component.literal(pad + "不稳定度: " + risk.instability()
+ " (基础 " + risk.recipeInstability() + " + 祭坛 " + risk.symmetry() + ")" + pad)
Component line2 = Component.literal(pad + "不稳定度: " + risk.instability() + pad)
.withStyle(style -> style.withColor(0xFFFFFFFF));
// 上下各留一个空行(垂直内边距)
@@ -383,19 +444,35 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
}
}
/** 右键:在玩家聊天框显示当前风险档位 + 描述。 */
/** 右键:聊天框提示。需手册 + 联网才显示详细风险;否则提示缺失项。 */
public void showDescriptionInChat(Player player) {
if (level == null || level.isClientSide()) return;
boolean hasBook = getBlockState().getValue(ModBlocks.BOOK);
boolean hasNetwork = getBlockState().getValue(ModBlocks.NETWORK);
if (!hasBook && !hasNetwork) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 缺少引导的书籍与网络能量(魔导手册与 AE 网络)"));
return;
}
if (!hasBook) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 需要放置魔导手册作为引导"));
return;
}
if (!hasNetwork) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 需要接入 AE 网络供能"));
return;
}
if (!cachedRisk.found()) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 未检测到注魔祭坛"));
return;
}
String tierWord = tierWord(cachedRisk.tier());
String desc = tierDesc(cachedRisk.tier());
// 档位行用档位颜色
Component msg = Component.literal("注魔风险: ")
.withStyle(style -> style.withColor(tierColor(cachedRisk.tier())))
.append(Component.literal(tierWord + " §f—— " + desc));
.append(Component.literal(tierWord + "§7"))
.append(Component.literal("\n不稳定度: " + cachedRisk.instability()
+ " (基础 " + cachedRisk.recipeInstability() + " + 祭坛 " + cachedRisk.symmetry() + ")§f\n"))
.append(Component.literal(desc));
player.sendSystemMessage(msg);
}
@@ -419,6 +496,16 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
return cachedRisk;
}
/** ME 网络是否在线(节点 active:有能量 + 有频道)。Jade 显示网络状态用。 */
public boolean isNetworkOnline() {
return mainNode.isActive();
}
/** 网格节点是否有能量。Jade 状态细分(在线/无频道/离线)用。 */
public boolean isNodePowered() {
return mainNode.isPowered();
}
// ===== IInWorldGridNodeHost =====
@Override
@@ -431,12 +518,12 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
}
public void markForUpdate() {
if (level != null) {
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
level.blockEntityChanged(worldPosition);
}
// 卸载/移除中跳过:sendBlockUpdated / blockEntityChanged 会把区块重新标记脏,停服保存时形成死循环
if (level == null || unloading || isRemoved()) return;
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
level.blockEntityChanged(worldPosition);
}
// ===== NBT =====
@@ -45,6 +45,10 @@ public final class ModBlockEntities {
REGISTRY.register("infusion_monitor",
() -> BlockEntityType.Builder.of(TileInfusionMonitor::new, ModBlocks.INFUSION_MONITOR.get()).build(null));
public static final Supplier<BlockEntityType<TileEssentiaProviderConnection>> ESSENTIA_PROVIDER_CONNECTION =
REGISTRY.register("essentia_provider_connection",
() -> BlockEntityType.Builder.of(TileEssentiaProviderConnection::new, ModBlocks.ESSENTIA_PROVIDER_CONNECTION.get()).build(null));
/**
* 虚拟的 Thaumcraft vis 源,供 vis 接口的提供端使用。
* 不作为真实方块放置;由 {@link thaumicenergistics.common.parts.VisInterfacePart}
@@ -297,4 +297,64 @@ public final class ModBlocks {
return java.util.List.of(new ItemStack(ModItems.ALKUSURE86_FUMO_ITEM.get()));
}
});
// ==================== 源质无线接收器 ====================
/** 是否已连接源质供应器(off/on 模型切换)。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty CONNECTED =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("connected");
/** 接收器六向贴放(贴墙/立地/挂顶)。 */
public static final net.minecraft.world.level.block.state.properties.DirectionProperty FACING_ALL =
net.minecraft.world.level.block.state.properties.BlockStateProperties.FACING;
/** 接收器专用:低硬度 → 镐子可快速挖掘。 */
private static final BlockBehaviour.Properties PC =
BlockBehaviour.Properties.of().mapColor(MapColor.STONE).strength(1.0f).sound(SoundType.STONE);
public static final DeferredBlock<Block> ESSENTIA_PROVIDER_CONNECTION = REGISTRY.register("essentia_provider_connection", () -> new ThEBaseEntityBlock(PC) {
@Override
public net.minecraft.world.level.block.entity.BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new TileEssentiaProviderConnection(pos, state);
}
@Override
public <T extends net.minecraft.world.level.block.entity.BlockEntity> net.minecraft.world.level.block.entity.BlockEntityTicker<T> getTicker(net.minecraft.world.level.Level level, BlockState state, net.minecraft.world.level.block.entity.BlockEntityType<T> type) {
if (!level.isClientSide() && type == ModBlockEntities.ESSENTIA_PROVIDER_CONNECTION.get()) {
return (lvl, pos, st, be) -> TileEssentiaProviderConnection.serverTick(lvl, pos, st, (TileEssentiaProviderConnection) be);
}
return null;
}
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(CONNECTED, FACING_ALL);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 正面朝外 = 点击面的方向(贴墙/立地/挂顶)
return this.defaultBlockState()
.setValue(FACING_ALL, context.getClickedFace())
.setValue(CONNECTED, false);
}
@Override
public net.minecraft.world.phys.shapes.VoxelShape getShape(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
// 薄碰撞箱(贴墙/立地/挂顶),不遮挡光线;UP=立地(底部) DOWN=挂顶(顶部)
return switch (state.getValue(FACING_ALL)) {
case NORTH -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.25, 0.75, 0.75, 0.75, 1.0);
case SOUTH -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.25, 0.0, 0.75, 0.75, 0.25);
case EAST -> net.minecraft.world.phys.shapes.Shapes.box(0.0, 0.25, 0.25, 0.25, 0.75, 0.75);
case WEST -> net.minecraft.world.phys.shapes.Shapes.box(0.75, 0.25, 0.25, 1.0, 0.75, 0.75);
case UP -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.0, 0.25, 0.75, 0.25, 0.75); // 立地:碰撞箱在底部
case DOWN -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.75, 0.25, 0.75, 1.0, 0.75); // 挂顶:碰撞箱在顶部
};
}
@Override
public int getLightBlock(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos) {
return 0; // 不遮挡光线
}
});
}
@@ -39,6 +39,7 @@ public final class ModCreativeTab {
output.accept(ModItems.KNOWLEDGE_INSCRIBER_ITEM.get()); output.accept(ModItems.ESSENTIA_CELL_WORKBENCH_ITEM.get());
output.accept(ModItems.DISTILLATION_ENCODER_ITEM.get());
output.accept(ModItems.INFUSION_MONITOR_ITEM.get());
output.accept(ModItems.ESSENTIA_PROVIDER_CONNECTION_ITEM.get()); output.accept(ModItems.WIRELESS_CONNECTOR.get());
// 玩偶不进创造物品栏:需 /give @p thaumicenergistics:alkusure86fumo 手动获取
// output.accept(ModItems.ALKUSURE86_FUMO_ITEM.get());
@@ -80,4 +80,11 @@ public final class ModItems {
public static final DeferredItem<BlockItem> DISTILLATION_ENCODER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.DISTILLATION_ENCODER);
public static final DeferredItem<BlockItem> INFUSION_MONITOR_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.INFUSION_MONITOR);
public static final DeferredItem<BlockItem> ALKUSURE86_FUMO_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ALKUSURE86_FUMO);
public static final DeferredItem<BlockItem> ESSENTIA_PROVIDER_CONNECTION_ITEM =
REGISTRY.registerSimpleBlockItem(ModBlocks.ESSENTIA_PROVIDER_CONNECTION);
/** 无线连接器:右键接收器选中,再右键源质供应器建立连接。 */
public static final DeferredItem<net.minecraft.world.item.Item> WIRELESS_CONNECTOR =
REGISTRY.register("wireless_connector",
() -> new thaumicenergistics.common.items.WirelessConnectorItem(new net.minecraft.world.item.Item.Properties()));
}
@@ -22,7 +22,8 @@ public class AspectIngredientHelper implements IIngredientHelper<AspectIngredien
@Override
public String getDisplayName(AspectIngredient ingredient) {
if (ingredient == null || ingredient.aspect() == null) return "Unknown";
return ingredient.aspect().getName();
// 用本地化显示名(displayName,中文环境显示中文),与 AE 终端内源质名称一致
return ingredient.aspect().displayName().getString();
}
@Override
@@ -33,7 +33,8 @@ public class AspectIngredientRenderer implements IIngredientRenderer<AspectIngre
return tooltip;
}
Aspect aspect = ingredient.aspect();
tooltip.add(Component.literal(aspect.getName()));
// 本地化显示名(中文环境显示中文),与 AE 终端内源质名称一致
tooltip.add(aspect.displayName());
tooltip.add(Component.literal(aspect.getTag()).withStyle(style -> style.withColor(0x888888)));
return tooltip;
}
@@ -0,0 +1,87 @@
package thaumicenergistics.integration.jei;
import java.util.List;
import java.util.Optional;
import net.minecraft.client.renderer.Rect2i;
import mezz.jei.api.gui.builder.IClickableIngredientFactory;
import mezz.jei.api.gui.handlers.IGuiContainerHandler;
import mezz.jei.api.runtime.IClickableIngredient;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.StackWithBounds;
import appeng.core.definitions.AEItems;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/**
* AE 界面(源质终端等)的 JEI GuiContainerHandler:
* 悬停仓库中的源质条目时,返回 {@link AspectIngredient} 作为可点击成分,
* 使 JEI 的 A 键收藏/配方查看能正确显示源质图标与名称。
* <p>
* 背景:源质是非物品/流体 AEKey,repo 中显示为 WrappedGenericStack 物品。
* 若不提供转换,JEI 收藏会退化为"包裹通用堆栈"(无图标、名字丢失)。
* 本处理不依赖 AE2-JEI-Integration 是否存在,ThE 自带即可生效。
*/
public class ThEEssentiaGuiHandler implements IGuiContainerHandler<AEBaseScreen<?>> {
@Override
public Optional<? extends IClickableIngredient<?>> getClickableIngredientUnderMouse(
IClickableIngredientFactory factory, AEBaseScreen<?> screen, double mouseX, double mouseY) {
StackWithBounds stackWithBounds = screen.getStackUnderMouse(mouseX, mouseY);
if (stackWithBounds == null) {
return Optional.empty();
}
AEssentiaKey key = unwrapEssentiaKey(stackWithBounds.stack());
if (key == null) {
return Optional.empty();
}
// AEssentiaKey id = thaumcraft:<tag> → Aspect
Aspect aspect = Aspect.getAspect(key.getId().getPath());
if (aspect == null) {
return Optional.empty();
}
return factory.createBuilder(AspectIngredientType.INSTANCE, new AspectIngredient(aspect))
.buildWithArea(stackWithBounds.bounds());
}
@Override
public List<Rect2i> getGuiExtraAreas(AEBaseScreen<?> screen) {
return List.of();
}
/**
* 从 GenericStack 中解析源质 key,兼容两种形式:
* <ul>
* <li>直接 {@link AEssentiaKey}(StackWithBounds.fromSlot 已 unwrap 的形式)</li>
* <li>{@code WrappedGenericStack} 物品包裹的形式(unwrap 失败/直接拿到物品的形式)</li>
* </ul>
*/
private static AEssentiaKey unwrapEssentiaKey(GenericStack stack) {
if (stack == null) {
return null;
}
AEKey what = stack.what();
if (what instanceof AEssentiaKey essentiaKey) {
return essentiaKey;
}
if (what instanceof AEItemKey itemKey) {
var itemStack = itemKey.toStack();
if (AEItems.WRAPPED_GENERIC_STACK.is(itemStack)) {
var unwrapped = GenericStack.unwrapItemStack(itemStack);
if (unwrapped != null && unwrapped.what() instanceof AEssentiaKey wrappedKey) {
return wrappedKey;
}
}
}
return null;
}
}
@@ -97,6 +97,12 @@ public class ThEJeiPlugin implements IModPlugin {
@Override
public void registerGuiHandlers(IGuiHandlerRegistration registration) {
// 源质终端(含无线版):悬停仓库源质 → 提供 AspectIngredient 可点击项,
// 使 JEI 的 A 键收藏/配方查看正确显示源质图标与名称(不依赖 AE2-JEI-Integration)。
registration.addGuiContainerHandler(
thaumicenergistics.common.container.GuiEssentiaTerminal.class,
new ThEEssentiaGuiHandler());
// 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能)
registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler());
// 知识记录仪:从 JEI 拉取物品到 3×3 合成网格(复用 GhostSlot + KnowledgeInscriberGhostSlotPacket)
@@ -22,6 +22,8 @@ import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import appeng.api.stacks.AEItemKey;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumcraft.integration.jei.JeiArcaneRecipe;
@@ -99,13 +101,19 @@ public class ThEUseArcaneRecipeTransfer
return null;
}
// 构建模板物品列表(9 格,对应 3x3 合成格)
// 构建模板物品列表(9 格,对应 3x3 合成格)——按可用变体挑选:
// 奥术配方 recipeId=null,服务端只能用客户端模板重建 Ingredient,
// 固定取 variants.get(0)(如石头)会导致有深板岩/圆石却拉不到。
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var inputs = recipe.inputs();
var stored = menu.snapshotStoredAmounts();
var craftable = menu.snapshotCraftableKeys();
var reserved = new Object2IntOpenHashMap<AEItemKey>();
for (int i = 0; i < templateItems.size() && i < inputs.size(); i++) {
var variants = inputs.get(i);
if (variants != null && !variants.isEmpty()) {
templateItems.set(i, variants.get(0).copyWithCount(1));
Ingredient ingredient = Ingredient.of(variants.stream());
templateItems.set(i, menu.pickAvailableTemplate(ingredient, stored, craftable, reserved));
}
}
@@ -26,7 +26,9 @@ import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import appeng.api.stacks.AEItemKey;
import appeng.util.CraftingRecipeUtil;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.world.inventory.MenuType;
import thaumicenergistics.ThaumicEnergistics;
@@ -123,15 +125,17 @@ public class ThEUseCraftingRecipeTransfer
}
// 实际转移:发送 ME 网络填充包
// 模板按可用变体挑选(网络有货→背包有货→可合成→首个),
// 否则固定取 items[0](如石头),网络里只有深板岩/圆石时槽位会拉不到东西。
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe);
var stored = menu.snapshotStoredAmounts();
var craftable = menu.snapshotCraftableKeys();
var reserved = new Object2IntOpenHashMap<AEItemKey>();
for (int i = 0; i < ingredients.size() && i < 9; i++) {
var ingredient = ingredients.get(i);
if (!ingredient.isEmpty()) {
var items = ingredient.getItems();
if (items.length > 0) {
templateItems.set(i, items[0].copyWithCount(1));
}
templateItems.set(i, menu.pickAvailableTemplate(ingredient, stored, craftable, reserved));
}
}
@@ -190,8 +194,6 @@ public class ThEUseCraftingRecipeTransfer
var slotView = inputSlots.get(i);
slotView.drawHighlight(guiGraphics,
missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR);
ThaumicEnergistics.LOG.info("[JEI-Transfer] showError slot={} missing={} craftable={}",
i, missing, craftable);
}
poseStack.popPose();
@@ -19,7 +19,6 @@ import appeng.crafting.execution.CraftingSubmitResult;
import net.minecraft.world.entity.player.Player;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.PatternResearchHelper;
/**
@@ -55,9 +54,6 @@ public abstract class MixinCraftingService {
continue; // 无研究要求
}
if (!TCResearchManager.has(player, research)) {
ThaumicEnergistics.LOG.info(
"[AE2ResearchGate] 玩家 {} 未解锁研究 {},拒绝合成请求 (pattern={})",
player.getName().getString(), research, pattern.getDefinition());
// 通过自定义网络包发送研究键到客户端(AE2 errorDetail 不跨网络传输)
if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
net.neoforged.neoforge.network.PacketDistributor.sendToPlayer(
@@ -50,7 +50,8 @@ public abstract class MixinMEStorageScreen {
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && hovered != null && self.getMenu().isPlayerSideSlot(hovered)) {
var slotStack = hovered.getItem();
if (slotStack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem) {
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
self.getMenu().containerId, hovered.getSlotIndex(), slotStack));
cir.setReturnValue(true);
@@ -116,23 +117,13 @@ public abstract class MixinMEStorageScreen {
}
}
/** 源质容器:实现 IEssentiaContainerItem 的物品,或空玻璃安瓿(glass_phial 不实现该接口但可装罐)。 */
/** 源质容器:仅要素安瓿与要素罐子(天域之华/水晶等按 Kind 排除)。 */
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")));
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(stack);
}
/** 若手持物品是源质容器且非空,返回 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;
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.hasEssentia(stack);
}
}
@@ -0,0 +1,128 @@
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.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import appeng.client.gui.me.common.RepoSlot;
import appeng.menu.me.common.MEStorageMenu;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket;
import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket;
/**
* 让 WCWT(ME 综合工作终端)的屏幕支持源质右键交互。
* <p>
* WCWT 的 {@code WirelessComprehensiveWorkTerminalScreen} 覆写了 {@code mouseClicked},
* 因此注入到 {@code MEStorageScreen} 的 {@code MixinMEStorageScreen} 不会生效——
* 需直接对 WCWT 屏幕注入。逻辑与 {@code MixinMEStorageScreen} 完全一致:
* <ul>
* <li>Shift+右键玩家背包格(源质容器)→ 存入网络。</li>
* <li>右键网络区域手持满容器 → 存入网络。</li>
* <li>手持空容器右键源质条目 → 取出装罐。</li>
* </ul>
*/
@Mixin(targets = "com.lhy.wcwt.client.WirelessComprehensiveWorkTerminalScreen", remap = false)
public abstract class MixinWCWTScreen {
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, remap = false)
private void thE_onMouseClickedEssentia(double xCoord, double yCoord, int btn,
CallbackInfoReturnable<Boolean> cir) {
if (btn != 1) {
return;
}
AbstractContainerScreen<?> self = (AbstractContainerScreen<?>) (Object) this;
MEStorageMenu menu = (MEStorageMenu) self.getMenu();
Slot hovered = self.getSlotUnderMouse();
boolean shiftDown = AbstractContainerScreen.hasShiftDown();
var carried = menu.getCarried();
// 源质物品来源:优先菜单 carried(拾取物品);为空时回退到玩家主手物品
ItemStack sourceStack = !carried.isEmpty() ? carried
: menu.getPlayerInventory().player.getMainHandItem();
boolean isContainer = isSourceContainer(sourceStack);
boolean containerHasEssentia = isContainer && hasContainerEssentia(sourceStack);
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && hovered != null && menu.isPlayerSideSlot(hovered)) {
var slotStack = hovered.getItem();
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
menu.containerId, hovered.getSlotIndex(), slotStack));
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(AE2 原版 Shift+右键快速移动)
return;
}
// 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互)
if (!shiftDown && hovered != null && menu.isPlayerSideSlot(hovered)) {
return;
}
// Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当作普通物品放入网络
if (shiftDown && isContainer) {
if (containerHasEssentia) {
clearSource(menu, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
menu.containerId, sourceStack));
}
cir.setReturnValue(true);
return;
}
// 普通右键网络区域:手持满容器 → 存入源质
if (containerHasEssentia) {
clearSource(menu, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
menu.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(
menu.containerId, key.getId(), sourceStack));
cir.setReturnValue(true);
return;
}
}
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(WCWT 原版右键)
}
/** 清空 source 所在位置(carried 或主手),防止被 AE2 后续当普通物品处理。 */
private static void clearSource(MEStorageMenu menu, ItemStack source, ItemStack carried) {
if (!carried.isEmpty() && carried == source) {
menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
} else {
menu.getPlayerInventory().player.getInventory().setItem(
menu.getPlayerInventory().player.getInventory().selected,
net.minecraft.world.item.ItemStack.EMPTY);
}
}
/** 源质容器:仅要素安瓿与要素罐子(天域之华/水晶等按 Kind 排除)。 */
private static boolean isSourceContainer(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(stack);
}
/** 若手持物品是源质容器且非空,返回 true。 */
private static boolean hasContainerEssentia(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.hasEssentia(stack);
}
}
@@ -0,0 +1,52 @@
package thaumicenergistics.mixins.eap;
import net.minecraft.world.item.ItemStack;
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 appeng.api.stacks.GenericStack;
import mezz.jei.api.ingredients.ITypedIngredient;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.integration.jei.AspectIngredient;
/**
* ExtendedAE-Plus 兼容(可选,eap 未安装时静默跳过):
* 其 {@code InputEvents.toGenericStack} 只认 ItemStack/FluidStack,
* 我们收藏的源质(AspectIngredient)会返回 null → 中键/Shift+左键 无效。
* 此处把源质转成 {@code GenericStack(AEssentiaKey)},让 eap 的中键拉起合成、拉取流程正常走通。
*/
@Mixin(targets = "com.extendedae_plus.client.InputEvents", remap = false)
public abstract class MixinEAPInputEvents {
@Inject(method = "toGenericStack(Ljava/lang/Object;)Lappeng/api/stacks/GenericStack;",
at = @At("HEAD"), cancellable = true, remap = false)
private static void thE$essentiaToGenericStack(Object typed, CallbackInfoReturnable<GenericStack> cir) {
if (!(typed instanceof ITypedIngredient<?> typedIngredient)) {
return;
}
Object value = typedIngredient.getIngredient();
AEssentiaKey key = null;
// 新收藏:AspectIngredient(ThEEssentiaGuiHandler 提供,有图标+中文名)
if (value instanceof AspectIngredient ai && ai.aspect() != null) {
Aspect aspect = ai.aspect();
var id = TCReflection.getAspectId(aspect);
if (id != null) {
key = AEssentiaKey.of(id);
}
} else if (value instanceof ItemStack itemStack) {
// 旧收藏/兜底:WrappedGenericStack 物品包裹的形式
GenericStack unwrapped = GenericStack.unwrapItemStack(itemStack);
if (unwrapped != null && unwrapped.what() instanceof AEssentiaKey wrappedKey) {
key = wrappedKey;
}
}
if (key != null) {
cir.setReturnValue(new GenericStack(key, 1));
}
}
}
@@ -13,7 +13,12 @@
},
"STORAGE": {
"left": 134,
"bottom": 163
"bottom": 162
},
"TOOLBOX": {
"left": 175,
"bottom": 84,
"grid": "BREAK_AFTER_3COLS"
}
},
"text": {
@@ -48,23 +53,43 @@
"terminalStyle": {
"header": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 0, 195, 17]
"srcRect": [0, 0, 194, 17]
},
"firstRow": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 17, 195, 18]
"srcRect": [0, 17, 194, 18]
},
"row": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 35, 195, 18]
"srcRect": [0, 35, 194, 18]
},
"lastRow": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 53, 195, 18]
"srcRect": [0, 53, 194, 18]
},
"bottom": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 71, 195, 180]
"srcRect": [0, 71, 194, 180]
}
},
"widgets": {
"clearCraftingGrid": {
"left": 103,
"bottom": 160
},
"clearToPlayerInv": {
"left": 113,
"bottom": 160
},
"viewCells": {
"left": 193,
"top": 0
},
"toolbox": {
"left": 174,
"bottom": 90,
"width": 59,
"height": 66
}
}
}
@@ -0,0 +1,22 @@
{
"multipart": [
{ "when": { "facing": "down" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "x": 90 } },
{ "when": { "facing": "up" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "x": -90 } },
{ "when": { "facing": "north" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 0 } },
{ "when": { "facing": "south" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 180 } },
{ "when": { "facing": "east" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 90 } },
{ "when": { "facing": "west" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 270 } },
{ "when": { "facing": "down", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "x": 90 } },
{ "when": { "facing": "up", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "x": -90 } },
{ "when": { "facing": "north", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 0 } },
{ "when": { "facing": "south", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 180 } },
{ "when": { "facing": "east", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 90 } },
{ "when": { "facing": "west", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 270 } },
{ "when": { "facing": "down", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "x": 90 } },
{ "when": { "facing": "up", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "x": -90 } },
{ "when": { "facing": "north", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 0 } },
{ "when": { "facing": "south", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 180 } },
{ "when": { "facing": "east", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 90 } },
{ "when": { "facing": "west", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 270 } }
]
}
@@ -1,4 +1,4 @@
{
{
"itemGroup.thaumicenergistics": "Thaumic Energistics",
"block.thaumicenergistics.essentia_provider": "Essentia Provider",
"block.thaumicenergistics.infusion_provider": "Infusion Provider",
@@ -114,6 +114,12 @@
"thaumicenergistics.jade.energy_output": "Output Limit: %s AE/tick",
"thaumicenergistics.jade.tank_full": "Tank full, generation stopped",
"thaumicenergistics.gui.distillation_encoder.not_scanned": "Not scanned",
"thaumicenergistics.gui.encode": "Encode",
"thaumicenergistics.gui.knowledge_inscriber.no_core": "No Core",
"thaumicenergistics.gui.knowledge_inscriber.invalid": "Invalid",
"thaumicenergistics.gui.knowledge_inscriber.full": "Full",
"thaumicenergistics.gui.knowledge_inscriber.save": "Save",
"thaumicenergistics.gui.knowledge_inscriber.delete": "Delete",
"thaumicenergistics.jei.transfer.missing": "Missing: %d",
"thaumicenergistics.jei.transfer.craftable": "Craftable: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "Use Ctrl to craft missing items",
@@ -124,10 +130,27 @@
"thaumicenergistics.jade.monitor.crafting": "Infusion in progress",
"thaumicenergistics.jade.monitor.shortages": "Essentia shortage: %s",
"thaumicenergistics.jade.monitor.essence_ok": "Essentia sufficient",
"thaumicenergistics.jade.monitor.tier": "Risk: %s (%d/5)",
"thaumicenergistics.jade.tier.1": "Very Low",
"thaumicenergistics.jade.tier.2": "Low",
"thaumicenergistics.jade.tier.3": "Medium",
"thaumicenergistics.jade.tier.4": "High",
"thaumicenergistics.jade.tier.5": "Very High",
"thaumicenergistics.jade.provider.network": "Network: %s",
"thaumicenergistics.jade.provider.online": "Online",
"thaumicenergistics.jade.provider.offline": "Offline",
"thaumicenergistics.jade.provider.no_channel": "No Channel",
"thaumicenergistics.jade.assembler.vis_source": "Essentia Source: %s",
"thaumicenergistics.jade.assembler.vis_connected": "Connected",
"thaumicenergistics.jade.assembler.vis_disconnected": "Not Connected",
"block.thaumicenergistics.alkusure86fumo": "Alkusure86 Fumo",
"tc.research_text.INFUSIONMONITOR.stage.1": "To observe is to command. You place the Thaumonomicon beside the altar, an observer peering into each pulse of the infusion — the stirring of aura, the opening of rifts, the first whisper of flux. Bound to an ME network, the observer truly opens its eyes.",
"tc.research_text.INFUSIONMONITOR.stage.2": "The Infusion Monitor takes the Thaumonomicon as its key and the network as its sight. Place the tome and connect to the network, and it shall hover above the void, truthfully telling the risk of the ritual — and on right-click, it whispers its deeper warnings.",
"tc.research_name.INFUSIONMONITOR": "Infusion Monitor",
"tc.research_name.GOLEMWIFIBACKPACK": "Golem WiFi Backpack"
"tc.research_name.GOLEMWIFIBACKPACK": "Golem WiFi Backpack",
"block.thaumicenergistics.essentia_provider_connection": "Wireless Essentia Receiver",
"item.thaumicenergistics.wireless_connector": "Wireless Connector",
"tc.research_text.ESSENTIAPROVIDER.connection.stage.1": "The Wireless Essentia Receiver can be mounted on any surface. Use the Wireless Essentia Binding Tool to link it to an Essentia Provider, relaying essentia to alchemy towers and stills at a distance. Each provider serves up to eight receivers within thirty-two blocks; each additional receiver costs five extra AE/t. The receiver can also feed essentia back into the network, up to sixteen per transfer.",
"tc.research_text.ESSENTIAPROVIDER.tool.stage.1": "The Wireless Essentia Binding Tool creates and breaks wireless links: sneak-right-click an unlinked receiver to select it, then sneak-right-click an Essentia Provider to bind; sneak-right-click a linked receiver to disconnect. A normal right-click shows the connection coordinates of either device."
}
@@ -1,4 +1,4 @@
{
{
"itemGroup.thaumicenergistics": "神秘能源",
"block.thaumicenergistics.essentia_provider": "源质供应器",
"block.thaumicenergistics.infusion_provider": "注魔供应器",
@@ -131,6 +131,12 @@
"thaumicenergistics.jade.energy_output": "输出上限: %s AE/tick",
"thaumicenergistics.jade.tank_full": "能量槽已满,停止发电",
"thaumicenergistics.gui.distillation_encoder.not_scanned": "未扫描",
"thaumicenergistics.gui.encode": "编码",
"thaumicenergistics.gui.knowledge_inscriber.no_core": "缺核",
"thaumicenergistics.gui.knowledge_inscriber.invalid": "无效",
"thaumicenergistics.gui.knowledge_inscriber.full": "已满",
"thaumicenergistics.gui.knowledge_inscriber.save": "保存",
"thaumicenergistics.gui.knowledge_inscriber.delete": "删除",
"thaumicenergistics.jei.transfer.missing": "缺少: %d",
"thaumicenergistics.jei.transfer.craftable": "可合成: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "按住 Ctrl 合成缺失物品",
@@ -141,9 +147,23 @@
"thaumicenergistics.jade.monitor.crafting": "注魔进行中",
"thaumicenergistics.jade.monitor.shortages": "源质缺口: %s",
"thaumicenergistics.jade.monitor.essence_ok": "源质充足",
"thaumicenergistics.jade.monitor.tier": "风险: %s(%d/5)",
"thaumicenergistics.jade.tier.1": "极低",
"thaumicenergistics.jade.tier.2": "低",
"thaumicenergistics.jade.tier.3": "中",
"thaumicenergistics.jade.tier.4": "高",
"thaumicenergistics.jade.tier.5": "极高",
"thaumicenergistics.jade.provider.network": "网络: %s",
"thaumicenergistics.jade.provider.online": "在线",
"thaumicenergistics.jade.provider.offline": "离线",
"thaumicenergistics.jade.provider.no_channel": "无频道",
"block.thaumicenergistics.alkusure86fumo": "Alkusure86 玩偶",
"tc.research_text.INFUSIONMONITOR.stage.1": "观测即是驾驭。你将魔导手册置于祭坛之侧,以侦测器窥探注魔的每一次脉动——灵光、裂隙、咒波的先兆,尽收眼底。当它与 ME 网络相连,观测者便真正睁开眼。",
"tc.research_text.INFUSIONMONITOR.stage.2": "韵魔观测器以魔导手册为钥、以网络为眼。放上手册并接入网络,它便会悬浮于虚空之中,如实告知你注魔之仪的风险——而按住右键,它将低语出更深层的告诫。",
"tc.research_name.INFUSIONMONITOR": "韵魔观测器",
"tc.research_name.GOLEMWIFIBACKPACK": "傀儡WiFi背包"
"tc.research_name.GOLEMWIFIBACKPACK": "傀儡WiFi背包",
"block.thaumicenergistics.essentia_provider_connection": "源质无线接收器",
"item.thaumicenergistics.wireless_connector": "源质无线绑定工具",
"tc.research_text.ESSENTIAPROVIDER.connection.stage.1": "源质无线接收器可挂置于任意表面,以源质无线绑定工具与源质供应器建立无线连接,将源质远程输送至炼金塔、蒸馏器等设施。每台供应器最多服务八台接收器,距离不得超过三十二格;每多一台接收器,供应器功耗上升五点。接收器亦可向网络回输源质,每次上限十六点。",
"tc.research_text.ESSENTIAPROVIDER.tool.stage.1": "源质无线绑定工具用于建立与解除无线连接:按住 Shift 右键未连接的接收器将其选中,再 Shift 右键源质供应器完成绑定;Shift 右键已连接的接收器可断开连接;普通右键则可查看双方连接坐标。"
}
@@ -0,0 +1,112 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"textures": {
"particle": "thaumicenergistics:block/essentia_provider_connection"
},
"elements": [
{
"from": [5, 5, 14],
"to": [11, 11, 16],
"faces": {
"north": {"uv": [0, 0, 6, 6], "texture": "#particle"},
"east": {"uv": [6, 6, 8, 0], "texture": "#particle"},
"south": {"uv": [0, 6, 6, 12], "texture": "#particle"},
"west": {"uv": [8, 0, 6, 6], "texture": "#particle"},
"up": {"uv": [6, 0, 8, 6], "rotation": 270, "texture": "#particle"},
"down": {"uv": [6, 0, 8, 6], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6.5, 6.5, 12],
"to": [9.5, 9.5, 12],
"faces": {
"north": {"uv": [6, 6, 9, 9], "texture": "#particle"}
}
},
{
"from": [6.25912, 7, 12.151],
"to": [7.25912, 9, 14.321],
"rotation": {"angle": -22.5, "axis": "y", "origin": [6.75912, 6.5, 12.681]},
"faces": {
"west": {"uv": [8, 0, 10, 2], "texture": "#particle"},
"up": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [9, 9, 8, 11], "texture": "#particle"}
}
},
{
"from": [8.74088, 7, 12.151],
"to": [9.74088, 9, 14.321],
"rotation": {"angle": 22.5, "axis": "y", "origin": [9.24088, 7.5, 12.681]},
"faces": {
"east": {"uv": [10, 0, 8, 2], "rotation": 270, "texture": "#particle"},
"up": {"uv": [8, 11, 9, 9], "texture": "#particle"},
"down": {"uv": [10, 7, 9, 9], "texture": "#particle"}
}
},
{
"from": [7, 5.83434, 12.0665],
"to": [9, 6.83434, 14.3065],
"rotation": {"angle": 22.5, "axis": "x", "origin": [8.4, 6.33434, 13.7065]},
"faces": {
"east": {"uv": [9, 11, 8, 9], "texture": "#particle"},
"west": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [10, 0, 8, 2], "texture": "#particle"}
}
},
{
"from": [7, 9.16566, 12.0665],
"to": [9, 10.16566, 14.2365],
"rotation": {"angle": -22.5, "axis": "x", "origin": [8.4, 9.66566, 13.7065]},
"faces": {
"east": {"uv": [9, 7, 10, 9], "texture": "#particle"},
"west": {"uv": [8, 9, 9, 11], "texture": "#particle"},
"up": {"uv": [8, 0, 10, 2], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6, 6, 11.5],
"to": [7, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [6, 9, 11.5],
"to": [7, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 6, 11.5],
"to": [10, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 9, 11.5],
"to": [10, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [12, 7, 9, 6], "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [9, 6, 12, 7], "rotation": 90, "texture": "#particle"}
}
}
]
}
@@ -0,0 +1,46 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"textures": {
"particle": "thaumicenergistics:block/essentia_provider_connection"
},
"elements": [
{
"from": [7.5, 7.5, 11],
"to": [8.5, 8.5, 12],
"faces": {
"east": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"west": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"up": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"down": {"uv": [9, 9, 10, 10], "texture": "#particle"}
}
},
{
"from": [7.1, 7.1, 9.2],
"to": [8.9, 8.9, 11],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [8, 2, 10, 4], "texture": "#particle"},
"east": {"uv": [10, 2, 8, 4], "texture": "#particle"},
"south": {"uv": [8, 4, 10, 6], "texture": "#particle"},
"west": {"uv": [8, 4, 10, 6], "rotation": 90, "texture": "#particle"},
"up": {"uv": [8, 4, 10, 2], "rotation": 180, "texture": "#particle"},
"down": {"uv": [10, 4, 8, 6], "texture": "#particle"}
}
},
{
"name": "cube_outline",
"from": [9.1, 9.1, 11.2],
"to": [6.9, 6.9, 9],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"east": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"south": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"west": {"uv": [8, 9, 6, 11], "texture": "#particle"},
"up": {"uv": [8, 11, 6, 9], "texture": "#particle"},
"down": {"uv": [6, 9, 8, 11], "texture": "#particle"}
}
}
]
}
@@ -0,0 +1,77 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"textures": {
"particle": "thaumicenergistics:block/essentia_provider_connection"
},
"elements": [
{
"from": [7.5, 7.5, 11],
"to": [8.5, 8.5, 12],
"faces": {
"east": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"west": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"up": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"down": {"uv": [9, 9, 10, 10], "texture": "#particle"}
}
},
{
"from": [7.1, 7.1, 9.2],
"to": [8.9, 8.9, 11],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [10, 4, 12, 6], "texture": "#particle"},
"east": {"uv": [12, 4, 10, 6], "texture": "#particle"},
"south": {"uv": [10, 2, 12, 4], "rotation": 90, "texture": "#particle"},
"west": {"uv": [10, 2, 12, 4], "texture": "#particle"},
"up": {"uv": [10, 6, 12, 4], "rotation": 180, "texture": "#particle"},
"down": {"uv": [10, 4, 12, 2], "rotation": 90, "texture": "#particle"}
}
},
{
"name": "cube_outline",
"from": [9.1, 9.1, 11.2],
"to": [6.9, 6.9, 9],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [10, 4, 11, 5], "texture": "#particle"},
"east": {"uv": [10, 4, 11, 5], "texture": "#particle"},
"south": {"uv": [10, 4, 11, 5], "texture": "#particle"},
"west": {"uv": [11, 4, 10, 5], "texture": "#particle"},
"up": {"uv": [11, 5, 10, 4], "texture": "#particle"},
"down": {"uv": [10, 4, 11, 5], "texture": "#particle"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [137.73, -0.85, -90.28],
"translation": [0, 2, 1.5],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [0, 45, 0],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [0, -135, 0],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"translation": [0, 3, 0],
"scale": [0.25, 0.25, 0.25]
},
"gui": {
"rotation": [30, -135, 0],
"scale": [0.625, 0.625, 0.625]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,188 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"textures": {
"particle": "thaumicenergistics:block/essentia_provider_connection"
},
"elements": [
{
"from": [5, 5, 14],
"to": [11, 11, 16],
"faces": {
"north": {"uv": [0, 0, 6, 6], "texture": "#particle"},
"east": {"uv": [6, 6, 8, 0], "texture": "#particle"},
"south": {"uv": [0, 6, 6, 12], "texture": "#particle"},
"west": {"uv": [8, 0, 6, 6], "texture": "#particle"},
"up": {"uv": [6, 0, 8, 6], "rotation": 270, "texture": "#particle"},
"down": {"uv": [6, 0, 8, 6], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6.5, 6.5, 12],
"to": [9.5, 9.5, 12],
"faces": {
"north": {"uv": [6, 6, 9, 9], "texture": "#particle"}
}
},
{
"from": [6.25912, 7, 12.151],
"to": [7.25912, 9, 14.321],
"rotation": {"angle": -22.5, "axis": "y", "origin": [6.75912, 6.5, 12.681]},
"faces": {
"west": {"uv": [8, 0, 10, 2], "texture": "#particle"},
"up": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [9, 9, 8, 11], "texture": "#particle"}
}
},
{
"from": [8.74088, 7, 12.151],
"to": [9.74088, 9, 14.321],
"rotation": {"angle": 22.5, "axis": "y", "origin": [9.24088, 7.5, 12.681]},
"faces": {
"east": {"uv": [10, 0, 8, 2], "rotation": 270, "texture": "#particle"},
"up": {"uv": [8, 11, 9, 9], "texture": "#particle"},
"down": {"uv": [10, 7, 9, 9], "texture": "#particle"}
}
},
{
"from": [7, 5.83434, 12.0665],
"to": [9, 6.83434, 14.3065],
"rotation": {"angle": 22.5, "axis": "x", "origin": [8.4, 6.33434, 13.7065]},
"faces": {
"east": {"uv": [9, 11, 8, 9], "texture": "#particle"},
"west": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [10, 0, 8, 2], "texture": "#particle"}
}
},
{
"from": [7, 9.16566, 12.0665],
"to": [9, 10.16566, 14.2365],
"rotation": {"angle": -22.5, "axis": "x", "origin": [8.4, 9.66566, 13.7065]},
"faces": {
"east": {"uv": [9, 7, 10, 9], "texture": "#particle"},
"west": {"uv": [8, 9, 9, 11], "texture": "#particle"},
"up": {"uv": [8, 0, 10, 2], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6, 6, 11.5],
"to": [7, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [6, 9, 11.5],
"to": [7, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 6, 11.5],
"to": [10, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 9, 11.5],
"to": [10, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [12, 7, 9, 6], "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [9, 6, 12, 7], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [7.5, 7.5, 11],
"to": [8.5, 8.5, 12],
"faces": {
"east": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"west": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"up": {"uv": [9, 9, 10, 10], "texture": "#particle"},
"down": {"uv": [9, 9, 10, 10], "texture": "#particle"}
}
},
{
"from": [7.1, 7.1, 9.2],
"to": [8.9, 8.9, 11],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [8, 2, 10, 4], "texture": "#particle"},
"east": {"uv": [10, 2, 8, 4], "texture": "#particle"},
"south": {"uv": [8, 4, 10, 6], "texture": "#particle"},
"west": {"uv": [8, 4, 10, 6], "rotation": 90, "texture": "#particle"},
"up": {"uv": [8, 4, 10, 2], "rotation": 180, "texture": "#particle"},
"down": {"uv": [10, 4, 8, 6], "texture": "#particle"}
}
},
{
"name": "cube_outline",
"from": [9.1, 9.1, 11.2],
"to": [6.9, 6.9, 9],
"rotation": {"angle": 45, "axis": "z", "origin": [8, 8, 10.1]},
"faces": {
"north": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"east": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"south": {"uv": [6, 9, 8, 11], "texture": "#particle"},
"west": {"uv": [8, 9, 6, 11], "texture": "#particle"},
"up": {"uv": [8, 11, 6, 9], "texture": "#particle"},
"down": {"uv": [6, 9, 8, 11], "texture": "#particle"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [141.59, -0.2, -55.02],
"translation": [0, 2, 1],
"scale": [0.375, 0.375, 0.375]
},
"thirdperson_lefthand": {
"rotation": [139.56, -0.16, -44.95],
"translation": [0, 1.75, 0.75],
"scale": [0.375, 0.375, 0.375]
},
"firstperson_righthand": {
"rotation": [68.36, 3.03, 5.76],
"translation": [-1, 4, 1.75],
"scale": [0.4, 0.4, 0.4]
},
"firstperson_lefthand": {
"rotation": [65.1, 1.97, 105.08],
"translation": [-0.5, 3.5, 1],
"scale": [0.4, 0.4, 0.4]
},
"ground": {
"rotation": [90, 0, 0],
"translation": [0, 4.5, 0]
},
"gui": {
"rotation": [108.74, -0.07, -58.99],
"translation": [0, 1.5, 0],
"scale": [1.005, 1.005, 1.005]
},
"head": {
"rotation": [90, 0, 0],
"translation": [0, 14.5, 0]
},
"fixed": {
"rotation": [90, 0, 0],
"translation": [0, 4, -0.75]
}
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "thaumicenergistics:item/wireless_connector"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@@ -9,6 +9,7 @@
"thaumicenergistics:essentia_cell_workbench",
"thaumicenergistics:distillation_encoder",
"thaumicenergistics:infusion_monitor",
"thaumicenergistics:essentia_provider_connection",
"thaumicenergistics:alkusure86fumo"
]
}
@@ -15,6 +15,8 @@
"client": [
"ae2.MixinCraftConfirmScreen",
"ae2.MixinMEStorageScreen",
"ae2.MixinWCWTScreen",
"eap.MixinEAPInputEvents",
"tc.ResearchPageIndexMixin",
"tc.MixinThaumonomiconScreen"
],
@@ -0,0 +1 @@
{}