Compare commits

..
11 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
1820982382 0aa6db226e [全流程完成] 2026-08-19 00:18:43 +08:00
1820982382 fc72ebca78 [全流程完成] 2026-08-19 00:17:24 +08:00
93 changed files with 10964 additions and 360 deletions
+4976 -2
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
# [Feature Request] 改善物品/方块扫描要素的自动赋予机制(恢复配方溯源 + 性能优化)
## 概述 Summary
请求为 TC4 1.21.1 移植版恢复 1.7.10 的**合成配方溯源**机制(物品作为配方产出时,按原料要素自动赋予要素),并修复 `getObjectTags` 无索引穷举遍历导致的严重性能问题。
当前移植版 `ItemAspectRegistry.getObjectTags` 只保留:注册表 → NBT → 精确表 → ID 词根推算 → `ENTROPY` 兜底。**词根表仅覆盖约 15 组 ID**,对 mod 物品几乎不命中,大量物品退化为 `ENTROPY×1` 或词根给的 1-2 种要素,且无法反映合成复杂度。
## 问题描述 Problem
### 功能缺失(影响整个 mod 生态)
1. **要素不准确**:mod 物品(如 `thaumicenergistics:essentia_provider`、`waystones:waystone`)扫描要素靠 ID 词根猜——词根对不上就 `ENTROPY×1`,对得上也只有 1-3 种
2. **无合成溯源**:合成材料完全不影响要素——1 个原料的和 10 个原料的合成品要素一样,无法体现合成复杂度
### 性能严重(实测)
- `getObjectTags` 每次调用**穷举遍历全部配方**(1932+ 个 CRAFTING)比较产出——无索引、无缓存
- 客户端 JEI 启动遍历全部物品时触发全量扫描:**实测单次扫描耗时 10 秒 / 1127 tick**(F3+L profiler)
- 频繁调用(Thaumonomicon/JEI 显示)导致持续卡顿
## 期望行为 Expected Behavior
- mod 物品扫描要素**按合成材料赋予**(与 1.7.10 一致)
- 要素数量/种类**合理**(种类 2-6、总量 8-24、单要素 ≤64)
- 扫描/显示**无卡顿**(单次查询毫秒级)
## 建议实现 Suggested Implementation
### A. 功能:getObjectTags 增加配方溯源分支
```
在词根推算 / ENTROPY 兜底之前:
recipe = 查产出=stack 的配方
if recipe != null:
原料要素 = 递归 getObjectTags(原料)(深度 ≤2,visited 防环)
返回 原料要素之和(同种要素取最大,避免累加爆炸)
```
可选覆盖配方类型(按需):普通合成(Crafting)、熔炉(Smelting)、注魔(Infusion)、炼金(Crucible)、奥术(Arcane)。
### B. 性能:产出→配方索引
```
懒构建 Map<Item, List<Recipe>>(按产出分组,首次查询时建一次)
getObjectTags 内 O(1) 查索引,不再每次穷举遍历全部配方
结果缓存 ConcurrentHashMap<Item, AspectList>,绑定 Server 引用(/reload 自动失效)
```
### C. 要素限制(贴近 1.7.10 + 平衡)
- 单要素 ≤64
- 种类 ≤6(按数量降序取前 6)
- 总量按复杂度归一化:`clamp(8 + 原料种类×2, 8, 24)`
- 同种要素多个原料出现时**取最大值**(不累加,防 1000+)
## 复现步骤 Steps to Reproduce
1. 安装任意含合成配方的 mod(如 waystones 传送石碑:3 黑曜石 + 3 石砖 + 1 传送石)
2. 用魔导透镜扫描该 mod 物品
3. 实际:`ENTROPY×1` 或词根 1-3 种(传送石碑给 1 混沌)
4. 期望:黑曜石(暗/虚空)+ 石砖(石头)+ 传送石(水晶/末影)要素
## 性能复现
1. 客户端启动(JEI 加载,遍历全部物品调 getObjectTags)
2. F3+L 采样 10 秒
3. 实际:`getObjectTags` 占比极高(穷举遍历配方),单物品 10s 级
## 参考实现要点(作者可参考)
- 1.21.1 `RecipeManager.byType` 为 **private**——用 `getAllRecipesFor(RecipeType)`(公开,返回 `List<RecipeHolder<T>>`)
- `RecipeHolder.id()` 直接返回 `ResourceLocation`(无 `.location()`)
- TC 配方访问:`ThaumcraftApi.getInfusionRecipe(result)` / `getCrucibleRecipe(result)` / `getCraftingRecipes()`(遍历 `IArcaneRecipe`,含 `getAspects()` vis 成本)
- `InfusionRecipe` 原料:`centralInput()` + `getComponents()`(公开)
- 缓存注意:组件敏感物品(药水/染色/耐久 NBT 变体)不进缓存(TC 的 NBT 分支先截走);COMPUTING 哨兵防跨配方递归环;缓存绑定 Server 引用(reload 失效)
## 环境 Environment
- Minecraft 1.21.1
- NeoForge 21.1.x
- Thaumcraft
## 补充
- 完整反编译调研与参考实现已由 ThaumicEnergistics 移植团队完成,可提供更详细方案(含存储体系:内存缓存 + 可选本地二进制文件缓存,指纹校验跨启动复用)
+29
View File
@@ -0,0 +1,29 @@
This is a community‑port of ThaumicEnergistics.
Modifications and port code: Copyright (c) 2026 AlkSur, Licensed under GNU LGPL‑3.0.
Original source code: Copyright (c) Chris, BrockWS, Licensed under MIT License (see LICENSE‑ORIGINAL‑MIT).
Original License:
The MIT License (MIT)
Copyright (c) 2018-2019 Chris, BrockWS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+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 -1
View File
@@ -1,11 +1,11 @@
# Project
mod_version=2.1.1
mod_group=thaumicenergistics
# Mod
mod_name=ThaumicEnergistics
mod_id=thaumicenergistics
mod_author=Nividica & Community
modLicense=LGPL-3.0
# Shared (must match root gradle.properties)
minecraft_version=1.21.1
@@ -1,5 +1,6 @@
package thaumicenergistics;
//
//AlkSur
import appeng.api.features.GridLinkables;
import appeng.api.features.P2PTunnelAttunement;
import appeng.api.parts.PartModels;
@@ -214,5 +215,10 @@ public final class ThaumicEnergistics {
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.ARCANE_ASSEMBLER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
// 注魔监视器:连接 AE 网络(network 状态)需要网格节点 capability
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.INFUSION_MONITOR.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
}
}
@@ -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
@@ -29,6 +29,8 @@ public final class RecipeRegistration {
public static InfusionRecipe ESSENTIA_PROVIDER;
public static InfusionRecipe INFUSION_PROVIDER;
public static InfusionRecipe ARCANE_ASSEMBLER;
/** 韵魔观测器注魔配方。 */
public static InfusionRecipe INFUSION_MONITOR;
// 奥术有序配方
public static ShapedArcaneRecipe STORAGE_COMPONENT_1K;
public static ShapedArcaneRecipe STORAGE_COMPONENT_4K;
@@ -50,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;
@@ -81,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),
@@ -208,6 +215,8 @@ public final class RecipeRegistration {
}
// ==================== 奥术无序配方 ====================
private static void registerShapelessArcane() {
// 1. 扩散核心
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
@@ -263,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"));
}
@@ -310,6 +335,19 @@ public final class RecipeRegistration {
tc("earth_shard"),
tc("air_shard"),
createFullVisScepter());
// 4. 韵魔观测器
// 中央:原版侦测器;外围:元始护符 + 揭示护目镜 + 世界盐×2。
// 需要灵气:魔力×16、灵气(AURA)×32、感官(SENSES, tag=sensus)×16。
// 不稳定度 3(配方基础,参与"总不稳定 = 基础 + 祭坛对称度",可自由调整:
// 1~2 低风险 / 3~5 中等 / 6~8 高)。
INFUSION_MONITOR = ThaumcraftApi.addInfusionCraftingRecipe("INFUSIONMONITOR", stack(ModBlocks.INFUSION_MONITOR),
3, new AspectList().add(Aspect.MAGIC, 16).add(Aspect.AURA, 32).add(Aspect.SENSES, 16),
new ItemStack(Items.OBSERVER),
tc("primal_charm"),
tc("goggles_revealing"),
tc("salis_mundus"),
tc("salis_mundus"));
}
/**
@@ -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");
}
}
@@ -0,0 +1,139 @@
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.TileInfusionMonitor;
import thaumicenergistics.common.tiles.TileInfusionMonitor.RiskData;
/**
* 韵魔观测器 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 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());
tag.putBoolean("crafting", risk.crafting());
tag.putInt("tier", risk.tier());
tag.putInt("instability", risk.instability());
tag.putInt("symmetry", risk.symmetry());
tag.putInt("recipeInstability", risk.recipeInstability());
tag.putString("tierText", risk.tierText());
tag.putString("shortages", risk.shortages());
tag.putInt("totalNeeded", risk.totalNeeded());
data.put(KEY, tag);
}
}
@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");
if (!found) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.no_matrix"));
return;
}
boolean crafting = tag.getBoolean("crafting");
int tier = tag.getInt("tier");
int instability = tag.getInt("instability");
int symmetry = tag.getInt("symmetry");
int recipeInstability = tag.getInt("recipeInstability");
String tierText = tag.getString("tierText");
String shortages = tag.getString("shortages");
int totalNeeded = tag.getInt("totalNeeded");
// 风险档位:纯文字(极低/低/中/高/极高),按档位着色
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.tier",
Component.translatable("thaumicenergistics.jade.tier." + tier), tier)
.withStyle(style -> style.withColor(tierColor(tier))));
// 不稳定度(分解:基础 + 祭坛)
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.instability",
instability, recipeInstability, symmetry));
// 档位文字(后果描述)
if (!tierText.isEmpty()) {
tooltip.add(Component.literal(tierText));
}
// 源质缺口
if (crafting) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.crafting"));
}
if (!shortages.isEmpty()) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.shortages", shortages));
} else if (totalNeeded > 0) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.essence_ok"));
}
}
private static int tierColor(int tier) {
return switch (tier) {
case 1 -> 0xFF55FF55; // 绿
case 2 -> 0xFFAAFF55; // 黄绿
case 3 -> 0xFFFFFF55; // 黄
case 4 -> 0xFFFFAA55; // 橙
default -> 0xFFFF5555; // 红
};
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "infusion_monitor");
}
}
@@ -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;
}
}
@@ -75,6 +75,7 @@ public final class ThEThaumcraft {
registerVisInterface();
registerEssentiaBuses();
registerInfusionProvider();
registerInfusionMonitor();
registerEssentiaProvider();
registerEssentiaTerminal();
registerDigisentia();
@@ -139,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) {
}
});
}
@@ -259,6 +259,20 @@ public final class ThEThaumcraft {
.registerResearchItem();
}
// ==================== 8. (-7,-2) INFUSIONMONITOR 韵魔观测器====================
// 前置: INFUSIONPROVIDER(注魔供应器);位于其左侧。无需研究比较,直接花费要素解锁。
private static void registerInfusionMonitor() {
new ResearchItem("INFUSIONMONITOR", TAB,
new AspectList().add(Aspect.MAGIC, 4).add(Aspect.AURA, 4).add(Aspect.SENSES, 6),
-7, -2, 1, new ItemStack(ModItems.INFUSION_MONITOR_ITEM.get()))
.setRound().setParents("INFUSIONPROVIDER")
.setPages(
new ResearchPage("tc.research_text.INFUSIONMONITOR.stage.1"),
new ResearchPage(RecipeRegistration.INFUSION_MONITOR),
new ResearchPage("tc.research_text.INFUSIONMONITOR.stage.2"))
.registerResearchItem();
}
// ==================== 8. (-2,-4) ESSENTIAPROVIDER 源质供应器====================
// 前置: ESSENTIABUSES(源质总线)
private static void registerEssentiaProvider() {
@@ -267,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);
}
}
}
}
@@ -46,10 +46,13 @@ public record EssentiaTerminalFillC2SPacket(int containerId, ResourceLocation as
&& ctx.player().containerMenu instanceof appeng.menu.me.common.MEStorageMenu menu
&& menu instanceof thaumicenergistics.common.container.ThEEssentiaMenuAccess acc) {
var player = menu.getPlayerInventory().player;
// 源质物品:优先包携带的 sourceStack(客户端真实来源);否则回退菜单 carried/主手
var source = !this.sourceStack.isEmpty() ? this.sourceStack
: (!menu.getCarried().isEmpty() ? menu.getCarried()
: player.getMainHandItem());
// ⚠ 必须用玩家真实栈(carried/主手)而非包里的拷贝:装罐会原地消耗空安瓿(shrink)
// 或修改罐子内容(setAspects)。若用 sourceStack(拷贝),消耗/修改不落真实物品——
// 空安瓿不消耗却能凭空得到满安瓿。
// 顺序与客户端一致:优先 carried(拾取),其次主手;sourceStack 仅兜底。
var source = !menu.getCarried().isEmpty() ? menu.getCarried()
: (!player.getMainHandItem().isEmpty() ? player.getMainHandItem()
: this.sourceStack);
thaumicenergistics.common.container.EssentiaFillHelper.fillContainerFromGrid(
acc.thE_getStorage(), acc.thE_getEnergySource(),
menu.getActionSource(), player, source, this.aspectId);
@@ -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
@@ -0,0 +1,199 @@
package thaumicenergistics.common.tiles;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IAspectContainer;
import thaumcraft.common.blockentities.InfusionMatrixBlockEntity;
import thaumcraft.common.blockentities.PedestalBlockEntity;
import thaumicenergistics.common.tiles.TileInfusionMonitor.RiskData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 注魔风险计算:扫描附近的注魔矩阵,按 5 档风险显示。
* <p>
* 判定依据(与 0.2.2.95 {@code InfusionMatrixBlockEntity} 源码一致):
* <ul>
* <li>单次异常判定概率 = instability / 500(每 10 tick 一次)。</li>
* <li>instability = 配方基础不稳定 + 祭坛对称度(上限 25)。</li>
* <li>源质不足会持续推高不稳定 → 源质缺口越大档位越高。</li>
* </ul>
*/
public final class InfusionRiskCalculator {
/** 扫描祭坛的半径。 */
private static final int MATRIX_SEARCH_RADIUS = 16;
/** 源质扫描半径(对应 EssentiaHandler.drainEssentia 的 range=12)。 */
private static final int ESSENTIA_SCAN_RADIUS = 12;
private InfusionRiskCalculator() {}
/** 附近注魔矩阵信息。 */
public record InfusionMatrixInfo(BlockPos matrixPos, InfusionMatrixBlockEntity matrix) {}
/** 扫描指定位置附近的注魔矩阵(取最近的)。 */
public static InfusionMatrixInfo scanNearbyMatrix(Level level, BlockPos center) {
InfusionMatrixBlockEntity nearest = null;
int bestDist = Integer.MAX_VALUE;
for (int dx = -MATRIX_SEARCH_RADIUS; dx <= MATRIX_SEARCH_RADIUS; dx++) {
for (int dz = -MATRIX_SEARCH_RADIUS; dz <= MATRIX_SEARCH_RADIUS; dz++) {
// 注魔矩阵在 y 方向位置固定(中央柱在下方 2 格),扫描 3 层高度
for (int dy = -4; dy <= 4; dy++) {
BlockPos pos = center.offset(dx, dy, dz);
if (level.getBlockEntity(pos) instanceof InfusionMatrixBlockEntity matrix) {
int dist = dx * dx + dz * dz + dy * dy;
if (dist < bestDist) {
bestDist = dist;
nearest = matrix;
}
}
}
}
}
return nearest != null ? new InfusionMatrixInfo(nearest.getBlockPos(), nearest) : null;
}
/**
* 计算风险数据。若祭坛正在注魔,用实时 instability;否则估算:
* 复制 matches() 匹配当前祭坛布局的配方(忽略研究门禁),得出配方基础不稳定 + 祭坛对称度。
*/
public static RiskData computeRisk(Level level, InfusionMatrixInfo info) {
InfusionMatrixBlockEntity matrix = info.matrix();
boolean crafting = matrix.crafting();
int symmetry = matrix.symmetry();
int recipeInstability;
int instability;
if (crafting) {
// 注魔中:instability() 实时(可能被源质不足推高)
instability = Math.min(25, matrix.instability());
recipeInstability = Math.max(0, instability - symmetry);
} else {
// 未开始:估算配方基础不稳定
recipeInstability = estimateRecipeInstability(level, info);
instability = Math.min(25, recipeInstability + symmetry);
}
// 源质缺口:剩余所需 vs 周围可抽
String shortages = "";
int totalNeeded = 0;
AspectList remaining = matrix.remainingEssentia();
if (remaining != null && remaining.size() > 0) {
Map<Aspect, Long> available = scanAvailableEssentia(level, info.matrixPos());
StringBuilder sb = new StringBuilder();
for (Aspect aspect : remaining.aspects()) {
int need = remaining.amount(aspect);
if (need <= 0) continue;
totalNeeded += need;
long have = available.getOrDefault(aspect, 0L);
if (have < need) {
if (!sb.isEmpty()) sb.append(",");
sb.append(aspect.getTag()).append(" 缺 ").append(need - have);
}
}
shortages = sb.toString();
}
int tier = tierFor(instability, shortages, totalNeeded);
return new RiskData(true, crafting, tier, instability, symmetry, recipeInstability,
tierText(tier), shortages, totalNeeded);
}
/** 5 档判定:不稳定度 + 源质缺口强制升级。 */
private static int tierFor(int instability, String shortages, int totalNeeded) {
int tier;
if (instability <= 1) tier = 1;
else if (instability <= 3) tier = 2;
else if (instability <= 6) tier = 3;
else if (instability <= 12) tier = 4;
else tier = 5;
// 源质缺口强制升级
if (!shortages.isEmpty()) {
// 缺口过半(总需 > 0 且缺口文字非空)→ 至少 4 档;严重 → 5 档
if (totalNeeded > 0 && shortages.length() > 20) {
tier = Math.max(tier, 5);
} else {
tier = Math.max(tier, 4);
}
}
return tier;
}
/** 档位对应文字(玩家视角后果)。 */
public static String tierText(int tier) {
return switch (tier) {
case 1 -> "基本安静,偶发弹飞 1 个物品";
case 2 -> "弹飞常见;闪电/咒波几乎不出现";
case 3 -> "弹飞+可能电击;咒波罕见";
case 4 -> "电击/爆炸/咒波都可能;可能销毁物品";
case 5 -> "频繁销毁/爆炸/咒波/精神污染——不要开始";
default -> "";
};
}
/** 未注魔时估算匹配配方的配方基础不稳定(复制 matches 但忽略研究门禁)。 */
private static int estimateRecipeInstability(Level level, InfusionMatrixInfo info) {
InfusionMatrixBlockEntity matrix = info.matrix();
try {
// 取中央柱物品(注魔矩阵正下方 2 格)
PedestalBlockEntity center = level.getBlockEntity(info.matrixPos().below(2)) instanceof PedestalBlockEntity ped
? ped : null;
if (center == null || center.heldStack().isEmpty()) {
return 0;
}
ItemStack central = center.heldStack();
// 收集周围柱物品(与 scanSurroundings 一致的 ±8 范围简化版)
List<ItemStack> components = new ArrayList<>();
for (int dx = -8; dx <= 8; dx++) {
for (int dz = -8; dz <= 8; dz++) {
if (dx == 0 && dz == 0) continue;
for (int dy = 1; dy <= 10; dy++) {
BlockEntity be = level.getBlockEntity(info.matrixPos().offset(dx, -dy, dz));
if (be instanceof PedestalBlockEntity ped && !ped.heldStack().isEmpty()) {
components.add(ped.heldStack());
break;
}
}
}
}
// 找匹配配方(忽略研究:直接比对中央 + 组件,不调 matches 以免研究门禁)
for (Object o : thaumcraft.api.ThaumcraftApi.getCraftingRecipes()) {
if (!(o instanceof thaumcraft.api.crafting.InfusionRecipe recipe)) continue;
if (!recipe.getRecipeInput().is(central.getItem())) continue;
// 简化:中央物品匹配即采用其基础不稳定(组件精确匹配留给 craftingStart 处理)
return recipe.instability(central);
}
} catch (Exception ignored) {
}
return 0;
}
/** 扫描周围 12 格内源质容器,汇总每种源质可抽量。 */
private static Map<Aspect, Long> scanAvailableEssentia(Level level, BlockPos matrixPos) {
Map<Aspect, Long> totals = new ConcurrentHashMap<>();
for (int dx = -ESSENTIA_SCAN_RADIUS; dx <= ESSENTIA_SCAN_RADIUS; dx++) {
for (int dy = -6; dy <= 6; dy++) {
for (int dz = -ESSENTIA_SCAN_RADIUS; dz <= ESSENTIA_SCAN_RADIUS; dz++) {
BlockEntity be = level.getBlockEntity(matrixPos.offset(dx, dy, dz));
if (be == null || be == level.getBlockEntity(matrixPos)) continue;
if (be instanceof IAspectContainer container) {
AspectList aspects = container.getAspects();
if (aspects == null) continue;
for (Aspect aspect : aspects.aspects()) {
int amt = aspects.getAmount(aspect);
if (amt > 0) totals.merge(aspect, (long) amt, Long::sum);
}
}
}
}
}
return totals;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,604 @@
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.network.chat.Component;
import net.minecraft.world.Container;
import net.minecraft.world.Containers;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Display;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.neoforged.neoforge.common.extensions.IPlayerExtension;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.EnumSet;
import java.util.List;
import appeng.api.networking.GridFlags;
import appeng.api.networking.GridHelper;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.IManagedGridNode;
import appeng.api.networking.IInWorldGridNodeHost;
import appeng.api.util.AECableType;
import net.minecraft.world.item.Items;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModBlocks;
/**
* 注魔监视器(灵气观测器)方块实体。
* <ul>
* <li>1 格物品槽:Shift+右键放入魔导手册(book 状态)。</li>
* <li>AE2 网格节点:连接 ME 网络后 network 状态=true。</li>
* <li>每 tick 扫描附近注魔矩阵,缓存风险数据供 Jade 显示(5 档风险/不稳定度/档位文字)。</li>
* </ul>
* 方块状态:
* <ol>
* <li>默认 → off 模型</li>
* <li>放手册 → off + book</li>
* <li>手册 + 连网 → off + book + on</li>
* <li>连网(无手册)→ off + Jade 显示</li>
* </ol>
*/
public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNodeHost {
/** 手册槽位。 */
public static final int BOOK_SLOT = 0;
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) {
@Override
public void setChanged() {
super.setChanged();
TileInfusionMonitor.this.setChanged();
TileInfusionMonitor.this.updateBookState();
}
};
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;
/** 悬浮气泡实体(text_display,服务端引用)。 */
private net.minecraft.world.entity.Display.TextDisplay bubble;
/** 气泡实体 Tag(用于坐标匹配查找)。 */
private static final String BUBBLE_TAG = "thaumicenergistics:monitor_bubble";
/** 气泡背景:半透明深灰(ARGB,Alpha=176)。 */
private static final int BUBBLE_BG_COLOR = 0xB0333333;
/** 气泡行宽(换行宽度)。中文全角字符约 12px,最长行"不稳定度: 9 (基础 5 + 祭坛 4)"约 240px——
* 必须大于最长行,否则行尾空格被换行逻辑丢弃、背景只包住分段 → 右侧看似截断。 */
private static final int BUBBLE_LINE_WIDTH = 320;
private final IGridNodeListener<TileInfusionMonitor> nodeListener = new IGridNodeListener<>() {
@Override
public void onSaveChanges(TileInfusionMonitor owner, IGridNode node) {
owner.setChanged();
}
@Override
public void onStateChanged(TileInfusionMonitor owner, IGridNode node, State state) {
if (state == State.POWER) {
owner.updateNetworkState();
}
owner.markForUpdate();
}
};
public TileInfusionMonitor(BlockPos pos, BlockState state) {
super(ModBlockEntities.INFUSION_MONITOR.get(), pos, state);
this.mainNode = GridHelper.createManagedNode(this, nodeListener)
.setVisualRepresentation(thaumicenergistics.init.ModItems.INFUSION_MONITOR_ITEM.get())
.setInWorldNode(true)
.setTagName("proxy")
.setFlags(GridFlags.REQUIRE_CHANNEL)
.setExposedOnSides(EnumSet.allOf(Direction.class));
this.mainNode.setIdlePowerUsage(40.0);
}
@Override
public void onLoad() {
super.onLoad();
unloading = false;
if (level != null && !level.isClientSide()) {
mainNode.create(level, getBlockPos());
}
updateBookState();
updateNetworkState();
}
@Override
public void setRemoved() {
unloading = true; // 先置标志再销毁网格节点:节点销毁回调会触发 updateNetworkState
super.setRemoved();
if (level != null && !level.isClientSide()) {
discardBubble();
}
if (mainNode != null) {
mainNode.destroy();
}
}
@Override
public void onChunkUnloaded() {
unloading = true; // 同上:卸载期间禁止改方块状态,否则 chunk 被重新标记脏 → 保存循环
super.onChunkUnloaded();
if (mainNode != null) {
mainNode.destroy();
}
}
@Override
public void onBreakBlock() {
super.onBreakBlock();
if (level != null && !level.isClientSide) {
discardBubble();
Containers.dropContents(level, worldPosition, inventory);
}
}
// ===== tick / 扫描 =====
public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) {
if (level.isClientSide()) return;
// 无祭坛缓存时全量扫描退避到 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();
}
be.updateNetworkState();
be.updateBubble();
}
/** 扫描附近注魔矩阵并缓存风险数据。
* 快速路径:已缓存矩阵仍在 → 只做 1 次 getBlockEntity 验证;
* 全量扫描(33×9×33≈9800 次 getBlockEntity)仅缓存失效时执行,是 mspt 大户。
* 风险重算限频(配方估算/源质汇总还要上千次查询),但注魔开始/结束瞬间(crafting 翻转)强制立即重算。 */
private void scanMatrix() {
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 (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) {
this.matrixPos = found;
markForUpdate();
}
}
// ===== 网格节点 / 状态同步 =====
private void updateNetworkState() {
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean net = mainNode.isActive();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.NETWORK) != net) {
level.setBlock(worldPosition, st.setValue(ModBlocks.NETWORK, net), 3);
// setBlock 后本 BE 仍存在;刷新引用
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
private void updateBookState() {
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.BOOK) != hasBook) {
level.setBlock(worldPosition, st.setValue(ModBlocks.BOOK, hasBook), 3);
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
// ===== 悬浮气泡(text_display 实体) =====
/**
* 每 tick 更新气泡:条件 = 放入魔导手册 且 连接 AE 网络 且 检测到注魔祭坛。
* 满足 → 生成/更新气泡;不满足 → 销毁气泡。
*/
private void updateBubble() {
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) {
// 文本未变则跳过反射/NBT 重建(原实现每 tick 都重建,是 mspt 开销之一)
net.minecraft.network.chat.Component text = buildBubbleText(cachedRisk);
if (!text.equals(lastBubbleText)) {
lastBubbleText = text;
applyBubbleNbt(text);
}
}
} else {
discardBubble();
lastBubbleText = null;
}
}
/** 确保气泡实体存在(不存在则生成;区块重载后按 Tag 找回旧实体)。 */
private void ensureBubble() {
if (bubble != null && bubble.isAlive()) return;
bubble = findBubble();
if (bubble != null) return;
bubble = EntityType.TEXT_DISPLAY.create(level);
if (bubble == null) return;
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));
bubble.addTag(BUBBLE_TAG);
level.addFreshEntity(bubble);
}
/** 反射调用 readAdditionalSaveData(CompoundTag) 配置气泡(缓存 Method)。 */
private static java.lang.reflect.Method READ_NBT_METHOD;
private void applyBubbleNbt(Component text) {
if (bubble == null || level == null) return;
try {
if (READ_NBT_METHOD == null) {
READ_NBT_METHOD = net.minecraft.world.entity.Entity.class
.getDeclaredMethod("readAdditionalSaveData", CompoundTag.class);
READ_NBT_METHOD.setAccessible(true);
}
CompoundTag nbt = new CompoundTag();
nbt.putString("text", Component.Serializer.toJson(text, level.registryAccess()));
nbt.putBoolean("default_background", false);
nbt.putInt("background", BUBBLE_BG_COLOR);
nbt.putString("billboard", "vertical");
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));
transform.put("translation", floatList(0F, 0F, 0F));
transform.put("left_rotation", floatList(0F, 0F, 0F, 1F));
transform.put("right_rotation", floatList(0F, 0F, 0F, 1F));
nbt.put("transformation", transform);
READ_NBT_METHOD.invoke(bubble, nbt);
} catch (Exception e) {
ThaumicEnergistics.LOG.warn("[Monitor] 气泡配置失败: {}", e.toString());
}
}
/** 构造 FloatTag 列表(1.21.1 CompoundTag 无 putFloatArray,需 ListTag)。 */
private static net.minecraft.nbt.ListTag floatList(float... values) {
net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag();
for (float v : values) {
list.add(net.minecraft.nbt.FloatTag.valueOf(v));
}
return list;
}
/** 按 Tag + 方块附近 AABB 查找已存在的气泡。 */
private net.minecraft.world.entity.Display.TextDisplay findBubble() {
if (level == null) return null;
AABB aabb = new AABB(worldPosition).inflate(0.15, 2.0, 0.15);
List<net.minecraft.world.entity.Display.TextDisplay> list = level.getEntitiesOfClass(
net.minecraft.world.entity.Display.TextDisplay.class, aabb,
e -> e.getTags().contains(BUBBLE_TAG));
return list.isEmpty() ? null : list.get(0);
}
/** 销毁气泡(方块拆除/条件不满足时)。 */
private void discardBubble() {
if (bubble != null && bubble.isAlive()) {
bubble.discard();
} else {
net.minecraft.world.entity.Display.TextDisplay found = findBubble();
if (found != null) {
found.discard();
}
}
bubble = null;
}
/** 风险数据 → 气泡多行文本(2 行:注魔风险/不稳定度;不显示基础+祭坛细分,细分在聊天栏)。 */
private static Component buildBubbleText(RiskData risk) {
// 内边距必须用不间断空格(\u00A0):普通空格在行尾会被字体布局裁剪,
// 导致背景只包住左侧、右侧贴边。NBSP 有真实字形,两侧都保留。
String pad = "\u00A0\u00A0"; // 左右内边距(NBSP)
String tierWord = tierWord(risk.tier());
Component line1 = Component.literal(pad + "注魔风险: " + tierWord + pad)
.withStyle(style -> style.withColor(tierColor(risk.tier())));
Component line2 = Component.literal(pad + "不稳定度: " + risk.instability() + pad)
.withStyle(style -> style.withColor(0xFFFFFFFF));
// 上下各留一个空行(垂直内边距)
return Component.literal("\n")
.append(line1).append("\n")
.append(line2).append("\n");
}
/** 5 档文字(无星星)。 */
private static String tierWord(int tier) {
return switch (tier) {
case 1 -> "极低";
case 2 -> "低";
case 3 -> "中";
case 4 -> "高";
case 5 -> "极高";
default -> "?";
};
}
/** 5 档描述(神秘时代风)。 */
private static String tierDesc(int tier) {
return switch (tier) {
case 1 -> "灵光如镜,仪式安然";
case 2 -> "灵光微澜,无碍仪式";
case 3 -> "符文低鸣,灵光流转不宁";
case 4 -> "裂隙于虚空中睁开,注视着祭坛";
case 5 -> "咒波翻涌,裂隙将吞没一切";
default -> "";
};
}
/** 档位颜色:1 绿 → 5 红。 */
private static int tierColor(int tier) {
return switch (tier) {
case 1 -> 0xFF55FF55;
case 2 -> 0xFFAAFF55;
case 3 -> 0xFFFFFF55;
case 4 -> 0xFFFFAA55;
default -> 0xFFFF5555;
};
}
/** 放入魔导手册(方块右键拦截后调用,防止书自身 use 打开界面)。 */
public void placeBook(Player player, ItemStack held) {
if (level == null || level.isClientSide()) return;
if (inventory.getItem(BOOK_SLOT).isEmpty()) {
inventory.setItem(BOOK_SLOT, held.copyWithCount(1));
if (!player.getAbilities().instabuild) {
held.shrink(1);
}
setChanged();
}
}
/** 取回魔导手册(Shift+右键)。 */
public void retrieveBook(Player player) {
if (level == null || level.isClientSide()) return;
ItemStack book = inventory.getItem(BOOK_SLOT);
if (!book.isEmpty()) {
if (!player.getInventory().add(book)) {
net.minecraft.world.Containers.dropItemStack(level, worldPosition.getX(), worldPosition.getY(), worldPosition.getZ(), book);
}
inventory.setItem(BOOK_SLOT, ItemStack.EMPTY);
setChanged();
}
}
/** 右键:聊天框提示。需手册 + 联网才显示详细风险;否则提示缺失项。 */
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 + "§7"))
.append(Component.literal("\n不稳定度: " + cachedRisk.instability()
+ " (基础 " + cachedRisk.recipeInstability() + " + 祭坛 " + cachedRisk.symmetry() + ")§f\n"))
.append(Component.literal(desc));
player.sendSystemMessage(msg);
}
public static boolean isThaumonomicon(ItemStack stack) {
if (stack.isEmpty()) return false;
return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "thaumonomicon")));
}
// ===== 访问器 =====
public Container getInventory() {
return inventory;
}
public boolean hasBook() {
return !inventory.getItem(BOOK_SLOT).isEmpty();
}
public RiskData getCachedRisk() {
return cachedRisk;
}
/** ME 网络是否在线(节点 active:有能量 + 有频道)。Jade 显示网络状态用。 */
public boolean isNetworkOnline() {
return mainNode.isActive();
}
/** 网格节点是否有能量。Jade 状态细分(在线/无频道/离线)用。 */
public boolean isNodePowered() {
return mainNode.isPowered();
}
// ===== IInWorldGridNodeHost =====
@Override
public IGridNode getGridNode(Direction dir) {
return mainNode.getNode();
}
public AECableType getCableConnectionType(Direction dir) {
return AECableType.SMART;
}
public void markForUpdate() {
// 卸载/移除中跳过:sendBlockUpdated / blockEntityChanged 会把区块重新标记脏,停服保存时形成死循环
if (level == null || unloading || isRemoved()) return;
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
level.blockEntityChanged(worldPosition);
}
// ===== NBT =====
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider reg) {
CompoundTag tag = super.getUpdateTag(reg);
saveRiskToTag(tag);
if (!inventory.getItem(BOOK_SLOT).isEmpty()) {
tag.put("Book", inventory.getItem(BOOK_SLOT).save(reg));
}
return tag;
}
@Override
public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider reg) {
super.handleUpdateTag(tag, reg);
loadRiskFromTag(tag);
if (tag.contains("Book")) {
inventory.setItem(BOOK_SLOT, ItemStack.parseOptional(reg, tag.getCompound("Book")));
}
}
private void saveRiskToTag(CompoundTag tag) {
tag.putBoolean("RiskFound", cachedRisk.found());
tag.putBoolean("RiskCrafting", cachedRisk.crafting());
tag.putInt("RiskTier", cachedRisk.tier());
tag.putInt("RiskInstability", cachedRisk.instability());
tag.putInt("RiskSymmetry", cachedRisk.symmetry());
tag.putInt("RiskRecipeInstability", cachedRisk.recipeInstability());
tag.putString("RiskTierText", cachedRisk.tierText());
tag.putString("RiskShortages", cachedRisk.shortages());
tag.putInt("RiskTotalNeeded", cachedRisk.totalNeeded());
}
private void loadRiskFromTag(CompoundTag tag) {
this.cachedRisk = new RiskData(
tag.getBoolean("RiskFound"),
tag.getBoolean("RiskCrafting"),
tag.getInt("RiskTier"),
tag.getInt("RiskInstability"),
tag.getInt("RiskSymmetry"),
tag.getInt("RiskRecipeInstability"),
tag.getString("RiskTierText"),
tag.getString("RiskShortages"),
tag.getInt("RiskTotalNeeded"));
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider reg) {
super.loadAdditional(tag, reg);
if (tag.contains("Book")) {
inventory.setItem(BOOK_SLOT, ItemStack.parseOptional(reg, tag.getCompound("Book")));
}
if (tag.contains("MatrixPos")) {
matrixPos = BlockPos.of(tag.getLong("MatrixPos"));
}
mainNode.loadFromNBT(tag);
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider reg) {
super.saveAdditional(tag, reg);
if (!inventory.getItem(BOOK_SLOT).isEmpty()) {
tag.put("Book", inventory.getItem(BOOK_SLOT).save(reg));
}
if (matrixPos != null) {
tag.putLong("MatrixPos", matrixPos.asLong());
}
mainNode.saveToNBT(tag);
}
/** 注魔风险缓存数据(Jade 用)。 */
public record RiskData(boolean found, boolean crafting, int tier, int instability, int symmetry,
int recipeInstability, String tierText, String shortages, int totalNeeded) {
public static final RiskData NONE = new RiskData(false, false, 0, 0, 0, 0, "", "", 0);
}
}
@@ -41,6 +41,14 @@ public final class ModBlockEntities {
REGISTRY.register("essentia_cell_workbench",
() -> BlockEntityType.Builder.of(TileEssentiaCellWorkbench::new, ModBlocks.ESSENTIA_CELL_WORKBENCH.get()).build(null));
public static final Supplier<BlockEntityType<TileInfusionMonitor>> INFUSION_MONITOR =
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}
@@ -8,12 +8,14 @@ import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredRegister;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.blocks.OrientableEntityBlock;
import thaumicenergistics.common.blocks.ThEBaseEntityBlock;
import thaumicenergistics.common.tiles.*;
import thaumicenergistics.init.ModItems;
import net.minecraft.network.chat.Component;
public final class ModBlocks {
@@ -119,4 +121,240 @@ public final class ModBlocks {
return null;
}
});
// ==================== 注魔监视器(灵气观测器) ====================
/** 是否放入魔导手册。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty BOOK =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("book");
/** 是否连接 AE 网络。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty NETWORK =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("network");
/**
* 注魔监视器:4 态(off / off+book / off+book+on / off+Jade),blockstate 用 multipart 合并模型。
*/
public static final DeferredBlock<Block> INFUSION_MONITOR = REGISTRY.register("infusion_monitor", () -> new ThEBaseEntityBlock(P) {
@Override
public net.minecraft.world.level.block.entity.BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new TileInfusionMonitor(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()) return null;
if (type == ModBlockEntities.INFUSION_MONITOR.get()) {
return (lvl, pos, st, be) -> TileInfusionMonitor.tick(lvl, pos, st, (TileInfusionMonitor) be);
}
return null;
}
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(BOOK, NETWORK, FACING);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 放置时始终朝向放置者
return this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite())
.setValue(BOOK, false)
.setValue(NETWORK, false);
}
@Override
protected net.minecraft.world.ItemInteractionResult useItemOn(net.minecraft.world.item.ItemStack stack, BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) {
if (level.getBlockEntity(pos) instanceof TileInfusionMonitor monitor) {
// 手持魔导手册 → 直接放入(返回 SUCCESS 拦截,避免魔导手册自身 use 打开界面)
if (TileInfusionMonitor.isThaumonomicon(stack)) {
if (!level.isClientSide) {
monitor.placeBook(player, stack);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// Shift+右键(非手册物品)→ 取回手册
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
monitor.retrieveBook(player);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 普通右键(非手册/非Shift)→ 聊天框显示风险描述
if (!level.isClientSide) {
monitor.showDescriptionInChat(player);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
return net.minecraft.world.ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
// 空手右键(1.21 走 useWithoutItem):Shift+空手 → 取回手册;普通空手右键 → 聊天框显示描述
@Override
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.phys.BlockHitResult hit) {
if (level.getBlockEntity(pos) instanceof TileInfusionMonitor monitor) {
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
monitor.retrieveBook(player);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
if (!level.isClientSide) {
monitor.showDescriptionInChat(player);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
return net.minecraft.world.InteractionResult.PASS;
}
@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) {
// 底座(高 0.25)+ 上部塔架(0.25~0.8)
return net.minecraft.world.phys.shapes.Shapes.or(
net.minecraft.world.phys.shapes.Shapes.box(0.0, 0.0, 0.0, 1.0, 0.25, 1.0),
net.minecraft.world.phys.shapes.Shapes.box(0.15, 0.25, 0.15, 0.85, 0.8, 0.85));
}
});
// ==================== Alkusure86 玩偶(Fumo) ====================
/** 水平朝向(韵魔观测器共用)。 */
public static final net.minecraft.world.level.block.state.properties.DirectionProperty FACING =
net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING;
// 姿态变体:0 = alkusure86fumo(默认坐姿),1 = a999csfumo。
public static final net.minecraft.world.level.block.state.properties.BooleanProperty VARIANT =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("variant");
// 玩偶专用:低硬度 → 空手可快速挖掘(装饰物)。
private static final BlockBehaviour.Properties PD =
BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_PINK).strength(0.2f).sound(SoundType.WOOL);
// Alkusure86 玩偶:纯装饰方块,2 个姿态模型 + 水平朝向。
// 右键切换姿态;Shift+右键拿起方块到手。
public static final DeferredBlock<Block> ALKUSURE86_FUMO = REGISTRY.register("alkusure86fumo", () -> new Block(PD) {
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, VARIANT);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 默认坐姿(variant=0),面向放置者
return this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite())
.setValue(VARIANT, false);
}
@Override
protected net.minecraft.world.ItemInteractionResult useItemOn(net.minecraft.world.item.ItemStack stack, BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) {
if (player.isShiftKeyDown()) {
// Shift+右键:拿起方块到手(或背包)
pickUp(level, pos, player);
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 右键:切换姿态
if (!level.isClientSide) {
level.setBlock(pos, state.setValue(VARIANT, !state.getValue(VARIANT)), 3);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 空手右键(1.21 走 useWithoutItem):Shift+空手拿起;右键切换姿态
@Override
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.phys.BlockHitResult hit) {
if (player.isShiftKeyDown()) {
pickUp(level, pos, player);
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
if (!level.isClientSide) {
level.setBlock(pos, state.setValue(VARIANT, !state.getValue(VARIANT)), 3);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
// Shift+右键拿起:移除方块,物品进主手(空手时)或背包。
private void pickUp(net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player) {
if (level.isClientSide) return;
ItemStack item = new ItemStack(ModItems.ALKUSURE86_FUMO_ITEM.get());
if (player.getMainHandItem().isEmpty()) {
player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, item);
} else if (!player.getInventory().add(item)) {
net.minecraft.world.Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, item);
}
level.removeBlock(pos, 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) {
// 玩偶整体占位:坐姿高度约 12 格单位(0.75 格),宽约 8(0.5 格)
return net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.0, 0.25, 0.75, 0.75, 0.75);
}
// 纯 Block 无战利品表 → 必须覆写 getDrops,否则挖掘不掉落
@Override
public java.util.List<ItemStack> getDrops(BlockState state, net.minecraft.world.level.storage.loot.LootParams.Builder builder) {
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; // 不遮挡光线
}
});
}
@@ -38,6 +38,10 @@ public final class ModCreativeTab {
output.accept(ModItems.ARCANE_ASSEMBLER_ITEM.get()); output.accept(ModItems.ESSENTIA_VIBRATION_CHAMBER_ITEM.get());
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());
// 源质虚拟物品不再在创造标签页中注册(避免玩家拿到物品栏)
// 它们通过 JEI 插件注册,仅作为 ghost ingredient 可拖拽到 AE 筛选槽
@@ -78,4 +78,13 @@ public final class ModItems {
public static final DeferredItem<BlockItem> KNOWLEDGE_INSCRIBER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.KNOWLEDGE_INSCRIBER);
public static final DeferredItem<BlockItem> ESSENTIA_CELL_WORKBENCH_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ESSENTIA_CELL_WORKBENCH);
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,12 @@
{
"variants": {
"facing=north,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 0 },
"facing=south,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 180 },
"facing=east,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 90 },
"facing=west,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 270 },
"facing=north,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 0 },
"facing=south,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 180 },
"facing=east,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 90 },
"facing=west,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 270 }
}
}
@@ -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 } }
]
}
@@ -0,0 +1,16 @@
{
"multipart": [
{ "when": { "facing": "north" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 180 } },
{ "when": { "facing": "south" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 0 } },
{ "when": { "facing": "east" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 270 } },
{ "when": { "facing": "west" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 90 } },
{ "when": { "facing": "north", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 180 } },
{ "when": { "facing": "south", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 0 } },
{ "when": { "facing": "east", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 270 } },
{ "when": { "facing": "west", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 90 } },
{ "when": { "facing": "north", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 180 } },
{ "when": { "facing": "south", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 0 } },
{ "when": { "facing": "east", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 270 } },
{ "when": { "facing": "west", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 90 } }
]
}
@@ -102,6 +102,8 @@
"tc.research_name.PSEUDO_SCEPTRE": "Crafting Scepters",
"tc.research_name.PSEUDO_COREGATHER": "Golem Core: Gather",
"gui.thaumicenergistics.ArcaneCraftingTerminal": "Arcane Crafting Terminal",
"thaumicenergistics.jade.current_aspect": "Fuel: %s",
"thaumicenergistics.jade.suction_type": "Suction: %s",
"thaumicenergistics.jade.essentia_stored": "Essentia: %d / %d",
"thaumicenergistics.jade.burning": "Burning %d%%, %.1f AE/tick",
@@ -112,7 +114,43 @@
"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"
"thaumicenergistics.jei.transfer.ctrl_click": "Use Ctrl to craft missing items",
"block.thaumicenergistics.infusion_monitor": "Infusion Monitor",
"thaumicenergistics.jade.monitor.title": "Infusion Monitor",
"thaumicenergistics.jade.monitor.no_matrix": "No infusion altar detected",
"thaumicenergistics.jade.monitor.instability": "Instability: %s (base %s + altar %s)",
"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",
"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."
}
@@ -28,7 +28,7 @@
"item.thaumicenergistics.essentia_storage_bus": "源质存储总线",
"item.thaumicenergistics.essentia_terminal": "源质终端",
"item.thaumicenergistics.arcane_crafting_terminal": "奥术合成终端",
"gui.thaumicenergistics.ArcaneCraftingTerminal": "奥术合成终端",
"gui.thaumicenergistics.ArcaneCraftingTerminal": "奥术合成终端",
"item.thaumicenergistics.essentia_level_emitter": "源质发信器",
"item.thaumicenergistics.vis_interface": "要素中继接口",
"tooltip.thaumicenergistics.essentia_import_bus.desc": "从相邻的源质容器抽取源质导入ME网络",
@@ -131,7 +131,39 @@
"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 合成缺失物品"
"thaumicenergistics.jei.transfer.ctrl_click": "按住 Ctrl 合成缺失物品",
"block.thaumicenergistics.infusion_monitor": "韵魔观测器",
"thaumicenergistics.jade.monitor.title": "韵魔观测器",
"thaumicenergistics.jade.monitor.no_matrix": "未检测到注魔祭坛",
"thaumicenergistics.jade.monitor.instability": "不稳定度: %s (基础 %s + 祭坛 %s)",
"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背包",
"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,216 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"render_type": "cutout",
"textures": {
"1": "thaumicenergistics:block/alkusure86fumo",
"particle": "thaumicenergistics:block/alkusure86fumo"
},
"elements": [
{
"name": "Head",
"from": [4.35, 4.48613, 8.33113],
"to": [11.65, 11.78613, 15.63113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.21113, 11.90613]},
"faces": {
"north": {"uv": [2, 2, 4, 4], "texture": "#1"},
"east": {"uv": [0, 2, 2, 4], "texture": "#1"},
"south": {"uv": [6, 2, 8, 4], "texture": "#1"},
"west": {"uv": [4, 2, 6, 4], "texture": "#1"},
"up": {"uv": [4, 2, 2, 0], "texture": "#1"},
"down": {"uv": [4, 0, 6, 2], "texture": "#1"}
}
},
{
"name": "Hat Layer",
"from": [4.1, 4.48613, 7.98113],
"to": [11.8, 12.08613, 15.68113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.21113, 11.90613]},
"faces": {
"north": {"uv": [10, 2, 12, 4], "texture": "#1"},
"east": {"uv": [8, 2, 10, 4], "texture": "#1"},
"south": {"uv": [14, 2, 16, 4], "texture": "#1"},
"west": {"uv": [12, 2, 14, 4], "texture": "#1"},
"up": {"uv": [12, 2, 10, 0], "texture": "#1"},
"down": {"uv": [12, 0, 14, 2], "texture": "#1"}
}
},
{
"name": "Left Arm",
"from": [3.89, -0.57401, 9.01167],
"to": [5.39, 5.42599, 11.01167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.42599, 10.01167]},
"faces": {
"north": {"uv": [9, 13, 9.75, 16], "texture": "#1"},
"east": {"uv": [8, 13, 9, 16], "texture": "#1"},
"south": {"uv": [10.75, 13, 11.5, 16], "texture": "#1"},
"west": {"uv": [9.75, 13, 10.5, 16], "texture": "#1"},
"up": {"uv": [9, 12, 9.75, 13], "texture": "#1"},
"down": {"uv": [9.75, 12, 10.5, 13], "texture": "#1"}
}
},
{
"name": "Left Arm Layer",
"from": [3.8154, -0.67401, 8.91167],
"to": [5.5154, 5.52599, 11.11167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.42599, 10.01167]},
"faces": {
"north": {"uv": [13, 13, 13.75, 16], "texture": "#1"},
"east": {"uv": [12, 13, 13, 16], "texture": "#1"},
"south": {"uv": [14.5, 13, 15.25, 16], "texture": "#1"},
"west": {"uv": [13.75, 13, 14.5, 16], "texture": "#1"},
"up": {"uv": [13, 12, 13.75, 13], "texture": "#1"},
"down": {"uv": [13.75, 12, 14.5, 13], "texture": "#1"}
}
},
{
"name": "RightArm",
"from": [10.48, -0.65754, 8.979],
"to": [11.98, 5.34246, 10.979],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.13663, 2.34246, 9.979]},
"faces": {
"north": {"uv": [11, 5, 11.75, 8], "texture": "#1"},
"east": {"uv": [10, 5, 11, 8], "texture": "#1"},
"south": {"uv": [12.75, 5, 13.5, 8], "texture": "#1"},
"west": {"uv": [11.75, 5, 12.5, 8], "texture": "#1"},
"up": {"uv": [11, 4, 11.75, 5], "texture": "#1"},
"down": {"uv": [12.5, 4, 11.75, 5], "texture": "#1"}
}
},
{
"name": "Right Arm Layer",
"from": [10.36163, -0.75754, 8.879],
"to": [12.06163, 5.44246, 11.079],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.16163, 2.34246, 9.979]},
"faces": {
"north": {"uv": [11, 9, 11.75, 12], "texture": "#1"},
"east": {"uv": [10, 9, 11, 12], "texture": "#1"},
"south": {"uv": [12.75, 9, 13.5, 12], "texture": "#1"},
"west": {"uv": [11.75, 9, 12.5, 12], "texture": "#1"},
"up": {"uv": [11, 8, 11.75, 9], "texture": "#1"},
"down": {"uv": [12.5, 8, 11.75, 9], "texture": "#1"}
}
},
{
"name": "Left Leg",
"from": [7.77, -0.23, -0.23],
"to": [10.23, 2.23, 6.23],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 4, 3, 5], "texture": "#1"},
"east": {"uv": [0, 8, 1, 5], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 4, 2, 5], "texture": "#1"},
"west": {"uv": [2, 5, 3, 8], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 8, 1, 5], "texture": "#1"},
"down": {"uv": [3, 5, 4, 8], "texture": "#1"}
}
},
{
"name": "Left Leg Layer",
"from": [7.67, -0.33, -0.33],
"to": [10.33, 2.33, 6.33],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 8, 3, 9], "texture": "#1"},
"east": {"uv": [0, 12, 1, 9], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 8, 2, 9], "texture": "#1"},
"west": {"uv": [2, 9, 3, 12], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 12, 1, 9], "texture": "#1"},
"down": {"uv": [3, 9, 4, 12], "texture": "#1"}
}
},
{
"name": "Right Leg",
"from": [5.77, -0.23, -0.23],
"to": [8.23, 2.23, 6.23],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [6, 12, 7, 13], "texture": "#1"},
"east": {"uv": [4, 16, 5, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [5, 12, 6, 13], "texture": "#1"},
"west": {"uv": [6, 13, 7, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [6, 16, 5, 13], "texture": "#1"},
"down": {"uv": [7, 13, 8, 16], "texture": "#1"}
}
},
{
"name": "Right Leg Layer",
"from": [5.67, -0.33, -0.33],
"to": [8.33, 2.33, 6.33],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 12, 3, 13], "texture": "#1"},
"east": {"uv": [0, 16, 1, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 12, 2, 13], "texture": "#1"},
"west": {"uv": [2, 13, 3, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 16, 1, 13], "texture": "#1"},
"down": {"uv": [3, 13, 4, 16], "texture": "#1"}
}
},
{
"name": "Body",
"from": [5.6, -1, 9.6],
"to": [10.4, 5, 12.4],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.4, 10.925]},
"faces": {
"north": {"uv": [5, 5, 7, 8], "texture": "#1"},
"east": {"uv": [4, 5, 5, 8], "texture": "#1"},
"south": {"uv": [8, 5, 10, 8], "texture": "#1"},
"west": {"uv": [7, 5, 8, 8], "texture": "#1"},
"up": {"uv": [5, 5, 7, 4], "texture": "#1"},
"down": {"uv": [7, 5, 9, 4], "texture": "#1"}
}
},
{
"name": "Body Layer",
"from": [5.5, -1.1, 9.5],
"to": [10.5, 5.1, 12.5],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.4, 10.925]},
"faces": {
"north": {"uv": [5, 9, 7, 12], "texture": "#1"},
"east": {"uv": [4, 9, 5, 12], "texture": "#1"},
"south": {"uv": [8, 9, 10, 12], "texture": "#1"},
"west": {"uv": [7, 9, 8, 12], "texture": "#1"},
"up": {"uv": [5, 9, 7, 8], "texture": "#1"},
"down": {"uv": [7, 9, 9, 8], "texture": "#1"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [60, 0, 0],
"translation": [0.25, 4.25, 1.25],
"scale": [0.7, 0.7, 0.7]
},
"thirdperson_lefthand": {
"rotation": [60, 0, 0],
"translation": [0.25, 4.25, 1.25],
"scale": [0.7, 0.7, 0.7]
},
"firstperson_righthand": {
"rotation": [0, 120, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_lefthand": {
"rotation": [0, 123, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"ground": {
"translation": [0, 2, 0],
"scale": [0.5, 0.5, 0.5]
},
"gui": {
"rotation": [15, 150, 0],
"translation": [-0.25, 0.25, 0],
"scale": [0.9, 0.9, 0.9]
},
"head": {
"translation": [12.75, 1.5, 0]
},
"fixed": {
"translation": [0, 0, -3.5]
}
}
}
@@ -0,0 +1,217 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"render_type": "cutout",
"textures": {
"1": "thaumicenergistics:block/alkusure86fumo",
"particle": "thaumicenergistics:block/alkusure86fumo"
},
"elements": [
{
"name": "Head",
"from": [4.35, 4.69613, 8.33113],
"to": [11.65, 11.99613, 15.63113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.42113, 11.90613]},
"faces": {
"north": {"uv": [2, 2, 4, 4], "texture": "#1"},
"east": {"uv": [0, 2, 2, 4], "texture": "#1"},
"south": {"uv": [6, 2, 8, 4], "texture": "#1"},
"west": {"uv": [4, 2, 6, 4], "texture": "#1"},
"up": {"uv": [4, 2, 2, 0], "texture": "#1"},
"down": {"uv": [4, 0, 6, 2], "texture": "#1"}
}
},
{
"name": "Hat Layer",
"from": [4.1, 4.69613, 7.98113],
"to": [11.8, 12.29613, 15.68113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.42113, 11.90613]},
"faces": {
"north": {"uv": [10, 2, 12, 4], "texture": "#1"},
"east": {"uv": [8, 2, 10, 4], "texture": "#1"},
"south": {"uv": [14, 2, 16, 4], "texture": "#1"},
"west": {"uv": [12, 2, 14, 4], "texture": "#1"},
"up": {"uv": [12, 2, 10, 0], "texture": "#1"},
"down": {"uv": [12, 0, 14, 2], "texture": "#1"}
}
},
{
"name": "Left Arm",
"from": [3.89, -0.36401, 9.01167],
"to": [5.39, 5.63599, 11.01167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.63599, 10.01167]},
"faces": {
"north": {"uv": [9, 13, 9.75, 16], "texture": "#1"},
"east": {"uv": [8, 13, 9, 16], "texture": "#1"},
"south": {"uv": [10.75, 13, 11.5, 16], "texture": "#1"},
"west": {"uv": [9.75, 13, 10.5, 16], "texture": "#1"},
"up": {"uv": [9, 12, 9.75, 13], "texture": "#1"},
"down": {"uv": [10.5, 12, 9.75, 13], "texture": "#1"}
}
},
{
"name": "Left Arm Layer",
"from": [3.8154, -0.46401, 8.91167],
"to": [5.5154, 5.73599, 11.11167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.63599, 10.01167]},
"faces": {
"north": {"uv": [13, 13, 13.75, 16], "texture": "#1"},
"east": {"uv": [12, 13, 13, 16], "texture": "#1"},
"south": {"uv": [14.5, 13, 15.25, 16], "texture": "#1"},
"west": {"uv": [13.75, 13, 14.5, 16], "texture": "#1"},
"up": {"uv": [13, 12, 13.75, 13], "texture": "#1"},
"down": {"uv": [13.75, 12, 14.5, 13], "texture": "#1"}
}
},
{
"name": "RightArm",
"from": [10.48, -0.44754, 8.979],
"to": [11.98, 5.55246, 10.979],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.13663, 2.55246, 9.979]},
"faces": {
"north": {"uv": [11, 5, 11.75, 8], "texture": "#1"},
"east": {"uv": [10, 5, 11, 8], "texture": "#1"},
"south": {"uv": [12.75, 5, 13.5, 8], "texture": "#1"},
"west": {"uv": [11.75, 5, 12.5, 8], "texture": "#1"},
"up": {"uv": [11, 4, 11.75, 5], "texture": "#1"},
"down": {"uv": [12.5, 4, 11.75, 5], "texture": "#1"}
}
},
{
"name": "Right Arm Layer",
"from": [10.36163, -0.54754, 8.879],
"to": [12.06163, 5.65246, 11.079],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.16163, 2.55246, 9.979]},
"faces": {
"north": {"uv": [11, 9, 11.75, 12], "texture": "#1"},
"east": {"uv": [10, 9, 11, 12], "texture": "#1"},
"south": {"uv": [12.75, 9, 13.5, 12], "texture": "#1"},
"west": {"uv": [11.75, 9, 12.5, 12], "texture": "#1"},
"up": {"uv": [11, 8, 11.75, 9], "texture": "#1"},
"down": {"uv": [12.5, 8, 11.75, 9], "texture": "#1"}
}
},
{
"name": "Left Leg",
"from": [7.77, -0.02, -0.23],
"to": [10.23, 2.44, 6.23],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 4, 3, 5], "texture": "#1"},
"east": {"uv": [0, 8, 1, 5], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 4, 2, 5], "texture": "#1"},
"west": {"uv": [2, 5, 3, 8], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 8, 1, 5], "texture": "#1"},
"down": {"uv": [3, 5, 4, 8], "texture": "#1"}
}
},
{
"name": "Left Leg Layer",
"from": [7.67, -0.12, -0.33],
"to": [10.33, 2.54, 6.33],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 8, 3, 9], "texture": "#1"},
"east": {"uv": [0, 12, 1, 9], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 8, 2, 9], "texture": "#1"},
"west": {"uv": [2, 9, 3, 12], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 12, 1, 9], "texture": "#1"},
"down": {"uv": [3, 9, 4, 12], "texture": "#1"}
}
},
{
"name": "Right Leg",
"from": [5.77, -0.02, -0.23],
"to": [8.23, 2.44, 6.23],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [6, 12, 7, 13], "texture": "#1"},
"east": {"uv": [4, 16, 5, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [5, 12, 6, 13], "texture": "#1"},
"west": {"uv": [6, 13, 7, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [6, 16, 5, 13], "texture": "#1"},
"down": {"uv": [7, 13, 8, 16], "texture": "#1"}
}
},
{
"name": "Right Leg Layer",
"from": [5.67, -0.12, -0.33],
"to": [8.33, 2.54, 6.33],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 12, 3, 13], "texture": "#1"},
"east": {"uv": [0, 16, 1, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 12, 2, 13], "texture": "#1"},
"west": {"uv": [2, 13, 3, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 16, 1, 13], "texture": "#1"},
"down": {"uv": [3, 13, 4, 16], "texture": "#1"}
}
},
{
"name": "Body",
"from": [5.6, -0.79, 9.6],
"to": [10.4, 5.21, 12.4],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.61, 10.925]},
"faces": {
"north": {"uv": [5, 5, 7, 8], "texture": "#1"},
"east": {"uv": [4, 5, 5, 8], "texture": "#1"},
"south": {"uv": [8, 5, 10, 8], "texture": "#1"},
"west": {"uv": [7, 5, 8, 8], "texture": "#1"},
"up": {"uv": [5, 5, 7, 4], "texture": "#1"},
"down": {"uv": [7, 5, 9, 4], "texture": "#1"}
}
},
{
"name": "Body Layer",
"from": [5.5, -0.89, 9.5],
"to": [10.5, 5.31, 12.5],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.61, 10.925]},
"faces": {
"north": {"uv": [5, 9, 7, 12], "texture": "#1"},
"east": {"uv": [4, 9, 5, 12], "texture": "#1"},
"south": {"uv": [8, 9, 10, 12], "texture": "#1"},
"west": {"uv": [7, 9, 8, 12], "texture": "#1"},
"up": {"uv": [5, 9, 7, 8], "texture": "#1"},
"down": {"uv": [7, 9, 9, 8], "texture": "#1"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [37.5, 0, 0],
"translation": [0.25, 3, 3],
"scale": [0.5, 0.5, 0.5]
},
"thirdperson_lefthand": {
"rotation": [42.75, 0, 0],
"translation": [0.25, 2.75, 3.5],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_righthand": {
"rotation": [0, 120, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_lefthand": {
"rotation": [0, 123, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"ground": {
"translation": [0, 2, 0],
"scale": [0.5, 0.5, 0.5]
},
"gui": {
"rotation": [15, 150, 0],
"translation": [0, 1.75, 0],
"scale": [0.95, 0.95, 0.95]
},
"head": {
"translation": [12.75, 1.5, 0]
},
"fixed": {
"rotation": [-21.5, 0, 0],
"translation": [0, 0, -3.5]
}
}
}
@@ -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,127 @@
{
"format_version": "1.21.6",
"credit": "Made with Blockbench",
"parent": "block/block",
"render_type": "cutout",
"texture_size": [64, 64],
"textures": {
"particle": "ae2:block/generics/back",
"model": "thaumicenergistics:block/infusion_monitor"
},
"elements": [
{
"from": [6.05, 8.99925, 3.0925],
"to": [9.95, 10.44225, 12.9075],
"faces": {
"north": {"uv": [7.5, 8.75, 8.5, 9.25], "texture": "#model"},
"east": {"uv": [8.5, 6, 11, 6.5], "texture": "#model"},
"south": {"uv": [7.5, 8.75, 8.5, 9.25], "texture": "#model"},
"west": {"uv": [8.5, 6, 11, 6.5], "texture": "#model"},
"up": {"uv": [2.5, 10.5, 1.5, 8], "texture": "#model"},
"down": {"uv": [3.5, 8, 2.5, 10.5], "texture": "#model"}
}
},
{
"from": [9.55658, 9.18425, 3.125],
"to": [15.40658, 9.83425, 12.875],
"rotation": {"angle": 22.5, "axis": "z", "origin": [10.20658, 10.08299, 6.375]},
"faces": {
"north": {"uv": [7.5, 8.5, 9, 8.75], "texture": "#model"},
"east": {"uv": [7, 6, 7.25, 8.5], "rotation": 270, "texture": "#model"},
"south": {"uv": [9, 8.5, 7.5, 8.75], "texture": "#model"},
"west": {"uv": [8.5, 6.5, 11, 6.75], "texture": "#model"},
"up": {"uv": [8.5, 8.5, 7, 6], "texture": "#model"},
"down": {"uv": [1.5, 8, 0, 10.5], "texture": "#model"}
}
},
{
"from": [8.44801, 9.34995, 3.775],
"to": [14.29801, 10.64995, 12.225],
"rotation": {"angle": 22.5, "axis": "z", "origin": [9.09801, 9.34995, 6.3425]},
"faces": {
"north": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"east": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"south": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"west": {"uv": [8.5, 7, 10.5, 7.25], "texture": "#model"},
"up": {"uv": [9.5, 2.75, 8, 0.75], "texture": "#model"}
}
},
{
"from": [7.142, 12.36275, 3.775],
"to": [12.992, 12.36275, 12.225],
"rotation": {"angle": 45, "axis": "z", "origin": [10.067, 12.36275, 8]},
"faces": {
"up": {"uv": [9.5, 0.75, 8, 2.75], "texture": "#model"},
"down": {"uv": [9.5, 0.75, 8, 2.75], "texture": "#model"}
}
},
{
"from": [3.008, 12.36925, 3.775],
"to": [8.858, 12.36925, 12.225],
"rotation": {"angle": -45, "axis": "z", "origin": [5.933, 12.36925, 8]},
"faces": {
"up": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"},
"down": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"}
}
},
{
"from": [2.04834, 11.09116, 3.775],
"to": [7.89834, 12.39116, 12.225],
"rotation": {"angle": -22.5, "axis": "z", "origin": [2.69834, 11.09116, 6.3425]},
"faces": {
"north": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"east": {"uv": [8.5, 7.25, 10.5, 7.5], "texture": "#model"},
"south": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"west": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"up": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"}
}
},
{
"from": [0.54702, 10.09853, 3.125],
"to": [6.39702, 10.74853, 12.875],
"rotation": {"angle": -22.5, "axis": "z", "origin": [3.47202, 10.42353, 8]},
"faces": {
"north": {"uv": [9, 8.5, 7.5, 8.75], "texture": "#model"},
"east": {"uv": [8.5, 6.75, 11, 7], "texture": "#model"},
"south": {"uv": [8.5, 7.5, 10, 7.75], "texture": "#model"},
"west": {"uv": [7.25, 8.5, 7, 6], "rotation": 270, "texture": "#model"},
"up": {"uv": [7, 8.5, 8.5, 6], "texture": "#model"},
"down": {"uv": [0, 8, 1.5, 10.5], "texture": "#model"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"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]
},
"head": {
"translation": [0, 14.25, 0]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,246 @@
{
"format_version": "1.21.6",
"credit": "Made with Blockbench",
"parent": "block/block",
"render_type": "cutout",
"texture_size": [64, 64],
"textures": {
"particle": "ae2:block/generics/back",
"model": "thaumicenergistics:block/infusion_monitor"
},
"elements": [
{
"name": "base1",
"from": [2, 0, 2],
"to": [14, 8, 14],
"faces": {
"north": {"uv": [7, 4, 10, 6], "texture": "#model"},
"east": {"uv": [7, 4, 10, 6], "texture": "#model"},
"south": {"uv": [7, 4, 10, 6], "texture": "#model"},
"west": {"uv": [7, 4, 10, 6], "texture": "#model"},
"up": {"uv": [7, 7, 4, 4], "texture": "#model"},
"down": {"uv": [7, 7, 4, 10], "texture": "#model", "cullface": "south"}
}
},
{
"name": "base2",
"from": [0, 1, 0],
"to": [16, 4, 16],
"faces": {
"north": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"east": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"south": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"west": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"up": {"uv": [4, 4, 0, 0], "texture": "#model"},
"down": {"uv": [4, 4, 0, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [-0.001, 0.999, 6.999],
"to": [0.001, 3.001, 9.001],
"faces": {
"east": {"uv": [8.5, 8, 9, 8.5], "texture": "#model"},
"west": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [6.999, 0.999, -0.001],
"to": [9.001, 3.001, 0.001],
"faces": {
"north": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"},
"south": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [15.999, 0.999, 6.999],
"to": [16.001, 3.001, 9.001],
"faces": {
"east": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"},
"west": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [6.999, 0.999, 15.999],
"to": [9.001, 3.001, 16.001],
"faces": {
"north": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"},
"south": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"}
}
},
{
"name": "base_light_bottom",
"from": [0, 1, 0],
"to": [16, 1, 16],
"faces": {
"up": {"uv": [8, 4, 4, 0], "texture": "#model"},
"down": {"uv": [8, 0, 4, 4], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [2.31462, 1.37727, 4],
"to": [4.31462, 5.37727, 12],
"rotation": {"angle": -22.5, "axis": "z", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"},
"south": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"west": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [11.68538, 1.37727, 4],
"to": [13.68538, 5.37727, 12],
"rotation": {"angle": 22.5, "axis": "z", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"east": {"uv": [11.5, 0.75, 9.5, 1.75], "texture": "#model"},
"south": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [4, 1.39257, 2.31158],
"to": [12, 5.39257, 4.31158],
"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"},
"east": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"},
"west": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [4, 1.37727, 11.68538],
"to": [12, 5.37727, 13.68538],
"rotation": {"angle": -22.5, "axis": "x", "origin": [8, 7.25, 8]},
"faces": {
"east": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"south": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"},
"west": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"}
}
},
{
"from": [3, 8, 3],
"to": [4, 9, 5],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [11, 8, 12],
"to": [12, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [12, 8, 11],
"to": [13, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [4, 8, 12],
"to": [5, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [3, 8, 11],
"to": [4, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [4, 8, 3],
"to": [5, 9, 4],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [12, 8, 3],
"to": [13, 9, 5],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [11, 8, 3],
"to": [12, 9, 4],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"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]
},
"head": {
"translation": [0, 14.25, 0]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,143 @@
{
"format_version": "1.21.6",
"credit": "Made with Blockbench",
"parent": "block/block",
"render_type": "translucent",
"texture_size": [64, 64],
"textures": {
"particle": "ae2:block/generics/back",
"lights": "ae2:block/molecular_assembler_lights"
},
"elements": [
{
"name": "cube_outline",
"from": [10.05, 10.54225, 13.0075],
"to": [5.95, 8.89925, 2.9925],
"faces": {
"north": {"uv": [2, 2, 3, 2.97], "rotation": 180, "texture": "#lights"},
"south": {"uv": [2, 13, 3, 14], "rotation": 180, "texture": "#lights"},
"up": {"uv": [3, 2.05, 2, 13.95], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [15.50658, 9.93425, 12.975],
"to": [9.45658, 9.08425, 3.025],
"rotation": {"angle": 22.5, "axis": "z", "origin": [10.20658, 10.08299, 6.375]},
"faces": {
"north": {"uv": [2, 2, 3, 13.95], "rotation": 180, "texture": "#lights"},
"south": {"uv": [3, 2, 2, 13.95], "rotation": 180, "texture": "#lights"},
"west": {"uv": [2.05, 14, 3, 2], "rotation": 180, "texture": "#lights"},
"up": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [2, 14, 3, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [6.49702, 10.84853, 12.975],
"to": [0.44702, 9.99853, 3.025],
"rotation": {"angle": -22.5, "axis": "z", "origin": [3.47202, 10.42353, 8]},
"faces": {
"north": {"uv": [3, 2.05, 2, 14], "rotation": 180, "texture": "#lights"},
"east": {"uv": [2.05, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"south": {"uv": [2, 2, 3, 13.95], "rotation": 180, "texture": "#lights"},
"up": {"uv": [3, 2, 2, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [3, 14, 2, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [14.39801, 10.74995, 12.325],
"to": [8.34801, 9.24995, 3.675],
"rotation": {"angle": 22.5, "axis": "z", "origin": [9.09801, 9.34995, 6.3425]},
"faces": {
"north": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"east": {"uv": [2, 2.05, 3, 14], "rotation": 180, "texture": "#lights"},
"south": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"west": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [2, 14, 3, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [7.99834, 12.49116, 12.325],
"to": [1.94834, 10.99116, 3.675],
"rotation": {"angle": -22.5, "axis": "z", "origin": [2.69834, 11.09116, 6.3425]},
"faces": {
"north": {"uv": [2, 2, 14, 3], "rotation": 180, "texture": "#lights"},
"east": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"south": {"uv": [2, 13, 14, 14], "rotation": 180, "texture": "#lights"},
"west": {"uv": [2, 2, 3, 13.95], "rotation": 180, "texture": "#lights"},
"up": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [3, 14, 2, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [13.092, 12.46275, 12.325],
"to": [7.042, 12.26275, 3.675],
"rotation": {"angle": 45, "axis": "z", "origin": [10.067, 12.36275, 8]},
"faces": {
"up": {"uv": [2, 2, 3, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [2, 14, 3, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline",
"from": [8.958, 12.46925, 12.325],
"to": [2.908, 12.26925, 3.675],
"rotation": {"angle": -45, "axis": "z", "origin": [5.933, 12.36925, 8]},
"faces": {
"up": {"uv": [3, 2, 2, 14], "rotation": 180, "texture": "#lights"},
"down": {"uv": [3, 14, 2, 2], "rotation": 180, "texture": "#lights"}
}
},
{
"name": "cube_outline_outline",
"from": [6.4, 7.9, 6.4],
"to": [9.6, 9.1, 9.6],
"faces": {
"north": {"uv": [2.1, 2.1, 2.85, 13.95], "rotation": 180, "texture": "#lights"},
"east": {"uv": [2.87, 2.03, 2.12, 13.98], "rotation": 180, "texture": "#lights"},
"south": {"uv": [2, 2.0025, 2.99, 13.9925], "rotation": 180, "texture": "#lights"},
"west": {"uv": [2.85, 2.1, 2.1, 13.95], "rotation": 180, "texture": "#lights"},
"up": {"uv": [2, 2, 3, 3], "rotation": 180, "texture": "#lights"},
"down": {"uv": [2, 2, 3, 3], "rotation": 180, "texture": "#lights"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"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]
},
"head": {
"translation": [0, 14.25, 0]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,3 @@
{
"parent": "thaumicenergistics:block/alkusure86fumo"
}
@@ -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,326 @@
{
"format_version": "1.21.6",
"credit": "Made with Blockbench",
"parent": "block/block",
"render_type": "cutout",
"texture_size": [64, 64],
"textures": {
"particle": "ae2:block/generics/back",
"model": "thaumicenergistics:block/infusion_monitor"
},
"elements": [
{
"name": "base1",
"from": [2, 0, 2],
"to": [14, 8, 14],
"faces": {
"north": {"uv": [7, 4, 10, 6], "texture": "#model"},
"east": {"uv": [7, 4, 10, 6], "texture": "#model"},
"south": {"uv": [7, 4, 10, 6], "texture": "#model"},
"west": {"uv": [7, 4, 10, 6], "texture": "#model"},
"up": {"uv": [7, 7, 4, 4], "texture": "#model"},
"down": {"uv": [7, 7, 4, 10], "texture": "#model", "cullface": "south"}
}
},
{
"name": "base2",
"from": [0, 1, 0],
"to": [16, 4, 16],
"faces": {
"north": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"east": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"south": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"west": {"uv": [8, 0, 12, 0.75], "texture": "#model"},
"up": {"uv": [4, 4, 0, 0], "texture": "#model"},
"down": {"uv": [4, 4, 0, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [-0.001, 0.999, 6.999],
"to": [0.001, 3.001, 9.001],
"faces": {
"east": {"uv": [8.5, 8, 9, 8.5], "texture": "#model"},
"west": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [6.999, 0.999, -0.001],
"to": [9.001, 3.001, 0.001],
"faces": {
"north": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"},
"south": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [15.999, 0.999, 6.999],
"to": [16.001, 3.001, 9.001],
"faces": {
"east": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"},
"west": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"}
}
},
{
"name": "base_light_side",
"from": [6.999, 0.999, 15.999],
"to": [9.001, 3.001, 16.001],
"faces": {
"north": {"uv": [8.5, 8.5, 9, 8], "texture": "#model"},
"south": {"uv": [9, 8.5, 8.5, 8], "texture": "#model"}
}
},
{
"name": "base_light_bottom",
"from": [0, 1, 0],
"to": [16, 1, 16],
"faces": {
"up": {"uv": [8, 4, 4, 0], "texture": "#model"},
"down": {"uv": [8, 0, 4, 4], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [2.31462, 1.37727, 4],
"to": [4.31462, 5.37727, 12],
"rotation": {"angle": -22.5, "axis": "z", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"},
"south": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"west": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [11.68538, 1.37727, 4],
"to": [13.68538, 5.37727, 12],
"rotation": {"angle": 22.5, "axis": "z", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"east": {"uv": [11.5, 0.75, 9.5, 1.75], "texture": "#model"},
"south": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [4, 1.39257, 2.31158],
"to": [12, 5.39257, 4.31158],
"rotation": {"angle": 22.5, "axis": "x", "origin": [8, 7.25, 8]},
"faces": {
"north": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"},
"east": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"},
"west": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"}
}
},
{
"name": "base_side",
"from": [4, 1.37727, 11.68538],
"to": [12, 5.37727, 13.68538],
"rotation": {"angle": -22.5, "axis": "x", "origin": [8, 7.25, 8]},
"faces": {
"east": {"uv": [7.5, 8.5, 7, 9.5], "texture": "#model"},
"south": {"uv": [9.5, 0.75, 11.5, 1.75], "texture": "#model"},
"west": {"uv": [7, 8.5, 7.5, 9.5], "texture": "#model"}
}
},
{
"from": [3, 8, 3],
"to": [4, 9, 5],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [11, 8, 12],
"to": [12, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [12, 8, 11],
"to": [13, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [4, 8, 12],
"to": [5, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [3, 8, 11],
"to": [4, 9, 13],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [4, 8, 3],
"to": [5, 9, 4],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [12, 8, 3],
"to": [13, 9, 5],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"east": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [11, 8, 3],
"to": [12, 9, 4],
"faces": {
"north": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"south": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"west": {"uv": [3.5, 9, 3.75, 9.25], "texture": "#model"},
"up": {"uv": [3.75, 9.25, 3.5, 9], "texture": "#model"}
}
},
{
"from": [6.05, 8.99925, 3.0925],
"to": [9.95, 10.44225, 12.9075],
"faces": {
"north": {"uv": [7.5, 8.75, 8.5, 9.25], "texture": "#model"},
"east": {"uv": [8.5, 6, 11, 6.5], "texture": "#model"},
"south": {"uv": [7.5, 8.75, 8.5, 9.25], "texture": "#model"},
"west": {"uv": [8.5, 6, 11, 6.5], "texture": "#model"},
"up": {"uv": [2.5, 10.5, 1.5, 8], "texture": "#model"},
"down": {"uv": [3.5, 8, 2.5, 10.5], "texture": "#model"}
}
},
{
"from": [9.55658, 9.18425, 3.125],
"to": [15.40658, 9.83425, 12.875],
"rotation": {"angle": 22.5, "axis": "z", "origin": [10.20658, 10.08299, 6.375]},
"faces": {
"north": {"uv": [7.5, 8.5, 9, 8.75], "texture": "#model"},
"east": {"uv": [7, 6, 7.25, 8.5], "rotation": 270, "texture": "#model"},
"south": {"uv": [9, 8.5, 7.5, 8.75], "texture": "#model"},
"west": {"uv": [8.5, 6.5, 11, 6.75], "texture": "#model"},
"up": {"uv": [8.5, 8.5, 7, 6], "texture": "#model"},
"down": {"uv": [1.5, 8, 0, 10.5], "texture": "#model"}
}
},
{
"from": [8.44801, 9.34995, 3.775],
"to": [14.29801, 10.64995, 12.225],
"rotation": {"angle": 22.5, "axis": "z", "origin": [9.09801, 9.34995, 6.3425]},
"faces": {
"north": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"east": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"south": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"west": {"uv": [8.5, 7, 10.5, 7.25], "texture": "#model"},
"up": {"uv": [9.5, 2.75, 8, 0.75], "texture": "#model"}
}
},
{
"from": [7.142, 12.36275, 3.775],
"to": [12.992, 12.36275, 12.225],
"rotation": {"angle": 45, "axis": "z", "origin": [10.067, 12.36275, 8]},
"faces": {
"up": {"uv": [9.5, 0.75, 8, 2.75], "texture": "#model"},
"down": {"uv": [9.5, 0.75, 8, 2.75], "texture": "#model"}
}
},
{
"from": [3.008, 12.36925, 3.775],
"to": [8.858, 12.36925, 12.225],
"rotation": {"angle": -45, "axis": "z", "origin": [5.933, 12.36925, 8]},
"faces": {
"up": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"},
"down": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"}
}
},
{
"from": [2.04834, 11.09116, 3.775],
"to": [7.89834, 12.39116, 12.225],
"rotation": {"angle": -22.5, "axis": "z", "origin": [2.69834, 11.09116, 6.3425]},
"faces": {
"north": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"east": {"uv": [8.5, 7.25, 10.5, 7.5], "texture": "#model"},
"south": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"west": {"uv": [8.5, 7.75, 10, 8], "texture": "#model"},
"up": {"uv": [8, 0.75, 9.5, 2.75], "texture": "#model"}
}
},
{
"from": [0.54702, 10.09853, 3.125],
"to": [6.39702, 10.74853, 12.875],
"rotation": {"angle": -22.5, "axis": "z", "origin": [3.47202, 10.42353, 8]},
"faces": {
"north": {"uv": [9, 8.5, 7.5, 8.75], "texture": "#model"},
"east": {"uv": [8.5, 6.75, 11, 7], "texture": "#model"},
"south": {"uv": [8.5, 7.5, 10, 7.75], "texture": "#model"},
"west": {"uv": [7.25, 8.5, 7, 6], "rotation": 270, "texture": "#model"},
"up": {"uv": [7, 8.5, 8.5, 6], "texture": "#model"},
"down": {"uv": [0, 8, 1.5, 10.5], "texture": "#model"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [75, 45, 0],
"translation": [0, 2.5, 0],
"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]
},
"head": {
"translation": [0, 14.25, 0]
},
"fixed": {
"scale": [0.5, 0.5, 0.5]
}
}
}
@@ -0,0 +1,6 @@
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "thaumicenergistics:item/wireless_connector"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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

@@ -7,6 +7,9 @@
"thaumicenergistics:essentia_vibration_chamber",
"thaumicenergistics:knowledge_inscriber",
"thaumicenergistics:essentia_cell_workbench",
"thaumicenergistics:distillation_encoder"
"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 @@
{}
+213
View File
@@ -0,0 +1,213 @@
# ThaumicEnergistics 代码位置速查表
> 说明:本文件所有路径均相对于 `modules/thaumicenergistics-neo/src/main/`。
> 若文件路径以 `java/` 开头 → Java 源码;以 `resources/assets/` 开头 → 资源/JSON。
---
## 一、物品 / 方块 Java 注册
| 你想找什么 | 文件 | 关键行 | 备注 |
|---|---|---|---|
| 物品总注册器入口 | `java/thaumicenergistics/init/ModItems.java` | **12** | `DeferredRegister.Items REGISTRY` |
| 1k/4k/16k/64k 源质存储单元(带容量的 Cell) | `ModItems.java` | 14~18 | `ESSENTIA_CELL_1K ~ ESSENTIA_CELL_CREATIVE` |
| 1k/4k/16k/64k 存储组件(Storage Component) | `ModItems.java` | 19~22 | `STORAGE_COMPONENT_1K ~ 64K` |
| 聚变核心 / 扩散核心 | `ModItems.java` | 23~24 | `COALESCENCE_CORE` / `DIFFUSION_CORE` |
| 铁齿轮 / 知识核心 / AE扳手核心 | `ModItems.java` | 25~27 | `IRON_GEAR` / `KNOWLEDGE_CORE` / `FOCUS_AEWRENCH` |
| 无线源质终端 / 傀儡WiFi背包 | `ModItems.java` | 28~29 | `WIRELESS_ESSENTIA_TERMINAL` / `GOLEM_WIFI_BACKPACK` |
| 源质IO总线/存储总线/终端/LevelEmitter/Vis接口/共鸣器 | `ModItems.java` | 30~37 | `ESSENTIA_IMPORT_BUS` 等一整套 |
| 方块对应物品(BlockItem) | `ModItems.java` | 39~47 | `ESSENTIA_PROVIDER_ITEM`、`KNOWLEDGE_INSCRIBER_ITEM` 等,与 ModBlocks 一一对应 |
| 方块总注册器入口 | `java/thaumicenergistics/init/ModBlocks.java` | **17** | `DeferredRegister.Blocks REGISTRY` |
| 源质提供器 Essentia Provider | `ModBlocks.java` | 20 | `ESSENTIA_PROVIDER` |
| 注入提供器 Infusion Provider | `ModBlocks.java` | 21 | `INFUSION_PROVIDER` |
| 奥术组装机 Arcane Assembler | `ModBlocks.java` | 22 | `ARCANE_ASSEMBLER`(Tile: TileArcaneAssembler) |
| 源质振动室 Essentia Vibration Chamber | `ModBlocks.java` | 23 | `ESSENTIA_VIBRATION_CHAMBER`(Tile: TileEssentiaVibrationChamber) |
| 知识刻印机 Knowledge Inscriber | `ModBlocks.java` | 24 | `KNOWLEDGE_INSCRIBER`(纯方块,无BE) |
| 精馏编码器 Distillation Encoder | `ModBlocks.java` | 25 | `DISTILLATION_ENCODER`(纯方块,无BE) |
| 齿轮箱 / 傀儡齿轮箱 | `ModBlocks.java` | 26~27 | `GEAR_BOX` / `GOLEM_GEAR_BOX` |
| 源质单元工作台 Essentia Cell Workbench(含 GUI 打开逻辑) | `ModBlocks.java` | 28~38 | 里面直接写了 `useItemOn` 和 `getMenuProvider` |
| 方块实体(BlockEntity)注册器 | `java/thaumicenergistics/init/ModBlockEntities.java` | — | (与 ModBlocks 里带 Tile 的对应) |
| 菜单类型 / 屏幕注册 | `java/thaumicenergistics/init/ModMenuTypes.java` + `ThaumicEnergistics.java:40~46` | — | `registerScreens` 方法里绑定 GuiXXX::new |
---
## 二、配方注册(1k/4k/16k/64k 组件、奥术、注入、坩埚)
| 你想找什么 | 文件 | 关键行 | 备注 |
|---|---|---|---|
| 配方总入口(commonSetup 里触发) | `java/thaumicenergistics/ThaumicEnergistics.java` | **36~38** | `commonSetup(event) { event.enqueueWork(RecipeRegistration::registerAll); }` |
| 配方注册总开关 | `java/thaumicenergistics/common/features/RecipeRegistration.java` | **247~253** | `registerAll()` 调 4 个子方法 + 打日志 |
| 有形状奥术合成(1k/4k/16k/64k、齿轮箱、刻印机等 14 条) | `RecipeRegistration.java` | **255~417** | `registerShapedArcane()` |
| ⭐ 1k Storage Component(任意天域之华版,for 循环展开) | `RecipeRegistration.java` | 256~267 | `wisps1k.get(i)` 当 'E' |
| ⭐ 4k Storage Component(任意天域之华版) | `RecipeRegistration.java` | 269~281 | `wisps4k.get(i)` 当 'E' |
| ⭐ 16k Storage Component(任意天域之华版) | `RecipeRegistration.java` | 283~296 | `wisps16k.get(i)` 当 'E' |
| 64k Storage Component | `RecipeRegistration.java` | 298~307 | 单配方,不用循环 |
| 天域之华名称表 + wispEssenceStacks()(不用 Tag,硬编码 ID 绕过时序) | `RecipeRegistration.java` | **Tag 常量区 + WISP_ESSENCE_VARIANTS 数组**(第 227 行起) | 这是配方「任意天域之华」能生效的核心 |
| 无形状奥术合成(核心、总线、终端等 7 条) | `RecipeRegistration.java` | 419~473 | `registerShapelessArcane()` |
| 注入配方(Essentia/Infusion Provider、奥术组装机) | `RecipeRegistration.java` | 475~514 | `registerInfusion()` |
| 坩埚配方(傀儡齿轮箱 = 齿轮箱熔炼) | `RecipeRegistration.java` | 516~523 | `registerCrucible()` |
| 3 个 helper:`stack()` 物品封装 / `tc()` 取 Thaumcraft 物品 / `ae()` 取 AE2 物品 | `RecipeRegistration.java` | 525~537 | 配方里所有 `'X', ae(...)`、`'X', tc(...)` 都走这里 |
| 配方乱码前缀 | `RecipeRegistration.java` | 所有 `addXxxRecipe` 第一个参数 | `"th_a€‰"` / `"th_b€‰"` / `"th_c€‰"` / `"th_d€‰"`(不影响功能但建议后续统一换成 `R + "xxx"`) |
---
## 三、资源引用链(材质 / 模型 / BlockState)
> 根目录:`resources/assets/thaumicenergistics/`
> 规则:**注册名 = 文件名(不含 .json/.png)**。物品/方块在 ModItems/ModBlocks 里用什么 `register("xxx")`,
> 下面 4 层 JSON/PNG 就必须都叫 `xxx`。
| 层 | 路径 | 它引用谁 | 关键字段 |
|---|---|---|---|
| ① BlockState(方块状态→方块模型) | `blockstates/<注册名>.json`(共 9 个) | 指向 `models/block/xxx.json` | `"model": "thaumicenergistics:block/<注册名>"` |
| ② Block Model(方块模型→PNG 贴图) | `models/block/<注册名>.json`(共 9 个) | 指向 `textures/block/xxx.png` | `"textures": { "up": "...", "down": "...", "north": "...", ... }` |
| ③ Item Model(物品模型→方块模型 **或** 物品 PNG) | `models/item/<注册名>.json`(物品 23 个 + 方块物品 9 个 = 32 个) | 方块物品:parent 到 ②;纯物品:直接写 layer0 | 方块物品:`"parent": "thaumicenergistics:block/<注册名>"`;纯物品:`"textures": { "layer0": "thaumicenergistics:item/<注册名>" }` |
| ④ PNG 贴图 | `textures/block/*.png`(9 套方块面)、`textures/item/*.png`(23 个物品贴图) | 被 ② ③ 引用 | 直接对应文件名 |
---
## 四、几个你最常问的具体定位(直接跳)
| 问题 | 去哪看 |
|---|---|
| 1k/4k/16k 组件配方「任意天域之华」怎么实现的 | `RecipeRegistration.java:227~244`(名称表 + `wispEssenceStacks()`) + `255~296`(for 循环注册) |
| 为什么要写死 `WISP_ESSENCE_VARIANTS` 数组,不能用 Tag 吗 | Tag 要到 DataPack 加载(比 commonSetup 晚)才填充,commonSetup 里 `getTag()` 返回空 → for 循环 0 次 → 配方直接没了。所以**注册阶段必须绕开 Tag 用硬编码 ID**。 |
| 「JEI 里完全没配方」排查顺序 | ① `latest.log` 里搜 `Wisp essence variants loaded` 看数字是不是 0;② 搜 `Invalid arcane recipe ingredient`(Ingredient 传进去就崩);③ 搜 `Mod loading has failed` 看 FMLCommonSetupEvent 是不是中途崩了;④ 确认 `commonSetup` 里是不是 `event.enqueueWork(RecipeRegistration::registerAll)` |
| Essentia Provider / Infusion Provider / Knowledge Inscriber / Gear Box「完全没材质」排查顺序 | 层 ①→②→③→④ 依次看:① blockstates 里 model 指向对不对;② models/block 里 textures 每个面都写了且 PNG 存在;③ models/item 里 parent 指回方块模型;④ textures 目录下 PNG 真实存在且命名一致。 |
| Essentia Cell Workbench「顶面材质没了,侧面正常」 | 直接打开 `models/block/essentia_cell_workbench.json` 看 `"textures"` 里 `"up"` 字段是不是写对了(应该指向 `textures/block/essentia_cell_workbench_top.png`) |
| 想加一个新物品/新方块 | ① 先在 ModItems / ModBlocks 里 `register("xxx", ...)`;② 想办法给物品弄一个 `models/item/xxx.json` + `textures/item/xxx.png`;如果是方块再加 `blockstates/xxx.json` + `models/block/xxx.json` + `textures/block/xxx.png`;③ 想进创造模式 Tab 要去 `ModCreativeTab.java` 里加 `output.accept(ModItems.XXX.get())`;④ 有配方去 `RecipeRegistration.java` 对应子方法里加一条 |
| 汉化文本 | `resources/assets/thaumicenergistics/lang/zh_cn.json` |
| 创造模式 Tab 物品顺序 | `java/thaumicenergistics/init/ModCreativeTab.java` |
| 源质单元容量实现(1k/4k/16k/64k 各自多少字节) | `java/thaumicenergistics/common/items/ItemEssentiaCell.java` 里的 `create1k / create4k / create16k / create64k` 静态工厂 |
| 天域之华 Tag 文件(防止主模组未来注册同名产生冲突) | `resources/data/thaumicenergistics/tags/items/wisp_essences.json` (里面写了 `"replace": false` 且包含 `{ "id": "#thaumcraft:wisp_essences", "required": false }` 作为兼容占位) |
| 启动崩 `Invalid arcane recipe ingredient: Ingredient@xxxx` | 一定是配方里把 Ingredient 传进了字符映射表(`'E', xxx` 的 xxx 必须是 ItemStack / Item / ResourceLocation,**不能是 Ingredient**),去 `registerShapedArcane()` 里搜 `wispEssenceIngredient`,有就立刻改成 `wisps.get(i)`(ItemStack) |
| Gradle 警告 `Space-assignment syntax ... init.gradle:10` | 这个是**全局 Gradle 初始化脚本**(`D:\GradleCache\init.gradle`)里的老写法,把 `url "https://..."` 改成 `url = "https://..."` 就好,跟模组代码无关 |
---
## 五、注册一个方块 — 从 Java 代码 → 材质 PNG 的完整 5 步引用链
> 🎯 例子用现成的 `knowledge_inscriber`(知识刻印机),你加新方块时直接把所有 `knowledge_inscriber` 替换成你的新注册 id 即可。
>
> 核心铁律:**注册 id 必须全局一致** — ModBlocks、ModItems、blockstates 文件名、models/block 文件名、models/item 文件名、textures 引用名,**全部都是同一个字符串**(大小写全小写,单词间下划线)。
### 第 1 步:Java 注册方块本体(决定注册 id)
**文件**:`java/thaumicenergistics/init/ModBlocks.java`
```java
public static final DeferredBlock<Block> KNOWLEDGE_INSCRIBER =
REGISTRY.register("knowledge_inscriber", () -> new Block(P));
// ^^^^^^^^^^^^^^^^^^^ ← 这里是「注册 id」,下面 4 层必须全一致
```
如果方块有 GUI(比如 Essentia Cell Workbench),就在 `new Block(P) { ... }` 里覆写 `useItemOn` + `getMenuProvider`。
如果方块有 BlockEntity(比如 Essentia Provider),就用 `ThEBaseEntityBlock(P)` 并实现 `newBlockEntity`。
### 第 2 步:Java 注册方块物品(拿在手里的那个 Item)
**文件**:`java/thaumicenergistics/init/ModItems.java`
```java
public static final DeferredItem<blockitem> KNOWLEDGE_INSCRIBER_ITEM =
REGISTRY.registerSimpleBlockItem(ModBlocks.KNOWLEDGE_INSCRIBER);
// ^^^^^^^^^^^^^^^^^^^ ← 直接绑定第 1 步的方块</blockitem>
```
`registerSimpleBlockItem` 是 NeoForge 封装好的,它会自动用**同一个注册 id**(这里就是 `knowledge_inscriber`)来生成 BlockItem,所以下面第 5 步「物品模型」的文件名也必须是 `knowledge_inscriber.json`。
### 第 3 步:BlockState(方块状态 → 方块模型的路由表)
**文件**:`resources/assets/thaumicenergistics/blockstates/knowledge_inscriber.json`
```json
{
"variants":
{
"":
{
"model": "thaumicenergistics:block/knowledge_inscriber"
}
}
}
```
> 📌 解释:Minecraft 渲染一个方块时,先看方块状态属性(朝向、开关等),再查 blockstates 找到该状态对应哪个**方块模型 JSON**。
> 知识刻印机没有状态属性(不区分朝向),所以用 `""` 表示「默认变体」;如果是有朝向的方块(比如 Essentia Provider),那要写 `facing=north/south/east/west/up/down` 六个变体。
### 第 4 步:Block Model(方块模型 → 每个面用哪张 PNG)
**文件**:`resources/assets/thaumicenergistics/models/block/knowledge_inscriber.json`
```json
{
"parent": "minecraft:block/cube_bottom_top",
"textures": {
"top": "thaumicenergistics:block/knowledge_inscriber_top",
"bottom": "thaumicenergistics:block/knowledge_inscriber_bottom",
"side": "thaumicenergistics:block/knowledge_inscriber_side"
}
}
```
> 📌 解释:`particle` 是「破坏方块时飞溅的粒子贴图」,一般跟 `north` 用同一张。上面 6 个面(`up/down/north/south/east/west`)的字符串,**不要加 `.png` 后缀**,游戏会自动去 `textures/` 目录下找对应的 PNG。
>
> 📌 对应的 3 张贴图必须真实存在:
> - `resources/assets/thaumicenergistics/textures/block/knowledge_inscriber_top.png`(顶面)
> - `resources/assets/thaumicenergistics/textures/block/knowledge_inscriber_bottom.png`(底面)
> - `resources/assets/thaumicenergistics/textures/block/knowledge_inscriber_side.png`(四个侧面共用一张)
### 第 5 步:Item Model(物品模型 — 方块物品最简单写法:直接「继承」方块模型)
**文件**:`resources/assets/thaumicenergistics/models/item/knowledge_inscriber.json`
```json
{
"parent": "thaumicenergistics:block/knowledge_inscriber"
}
```
> 📌 解释:99% 的方块物品**不需要独立物品贴图**,直接 `parent` 指回第 4 步的方块模型,Minecraft 会自动把方块渲染器缩放成 3D 物品形态(就是你在背包、物品栏、地上掉落物看到的样子)。
>
> 只有当你想让「物品形态」跟「方块形态」长得不一样时(比如 AE2 的接口物品是 2D 图标),才需要单独写 layer0 引用物品 PNG(写法参考下一节纯物品)。
### ✅ 新方块加完后**还要做的 2 件事**(别漏)
1. **进创造 Tab**:`java/thaumicenergistics/init/ModCreativeTab.java` 的 `displayItems` 里补一行 `output.accept(ModItems.KNOWLEDGE_INSCRIBER_ITEM.get());`(不然拿不到)
2. **汉化**:`resources/assets/thaumicenergistics/lang/zh_cn.json` 里补 `","block.thaumicenergistics.knowledge_inscriber":"知识刻印机"`
3. (可选)**有配方**:去 `RecipeRegistration.java` 的 `registerShapedArcane()` 或 `registerInfusion()` 里加一条。
---
## 六、注册一个纯物品(不是方块) — 从 Java 代码 → 材质 PNG 的完整 3 步引用链
> 🎯 例子用现成的 `storage_component_1k`(1k 存储组件),加新物品时直接替换 id 即可。
> 核心铁律同上:注册 id = 模型文件名 = PNG 文件名(不加后缀)。
### 第 1 步:Java 注册物品本体
**文件**:`java/thaumicenergistics/init/ModItems.java`
```java
public static final DeferredItem<item> STORAGE_COMPONENT_1K = REGISTRY.registerSimpleItem("storage_component_1k");
// ^^^^^^^^^^^^^^^^^^^^^ ← 注册 id,下面 2 层必须一致</item>
```
如果物品有自定义逻辑(比如源质 Cell 带容量读写 NBT、右键使用、背包叠加数量 1 等),就不用 `registerSimpleItem`,改用:
```java
public static final DeferredItem<itemessentiacell> ESSENTIA_CELL_1K =
REGISTRY.registerItem("essentia_cell_1k", ItemEssentiaCell::create1k);
```
本质上只是把 `new Item()` 换成你自己的自定义物品构造函数 / 静态工厂。
### 第 2 步:Item Model(物品模型 → layer0 指向物品 PNG)
**文件**:`resources/assets/thaumicenergistics/models/item/storage_component_1k.json`
```json
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "thaumicenergistics:item/storage_component_1k"
}
}
```
> 📌 解释:所有「普通 2D 贴图物品」的 parent 都是 `item/generated`(MC 内置的平面图标渲染器)。
> `layer0` 就是「主贴图」。字符串**不要加 `.png` 后缀**,游戏会自动找 `textures/item/storage_component_1k.png`。
>
> 少数物品有动画(比如 `knowledge_core.png.mcmeta`),那是在 PNG 旁边放一个 `.mcmeta` JSON 描述帧动画,模型文件写法不变。
### 第 3 步:放真实贴图 PNG
**文件**:`resources/assets/thaumicenergistics/textures/item/storage_component_1k.png`
> 📌 要求:16×16 或 32×32 或 64×64 的 PNG(尺寸必须是 2 的幂),**文件名必须跟 layer0 写的一样**。