This commit is contained in:
1820982382
2026-09-23 14:39:07 +08:00
parent ad0ead9f53
commit 0efd437209
20 changed files with 5589 additions and 114 deletions
+4973
View File
File diff suppressed because it is too large Load Diff
+23 -7
View File
@@ -21,7 +21,6 @@ repositories {
} }
} }
version = mod_version
group = mod_group group = mod_group
var modName = mod_name var modName = mod_name
@@ -45,9 +44,11 @@ neoForge {
dependencies { dependencies {
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api" compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api"
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17" // 加这行保留!!让AE2内部类可编译 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/jei-1.21.1-neoforge-19.39.0.368.jar')
compileOnly files('library/ae2jeiintegration-1.2.1.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}" compileOnly "maven.modrinth:jade:${jade_version}"
} }
@@ -57,7 +58,8 @@ tasks.withType(JavaCompile).configureEach {
it.options.release = 21 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 loaderRange = project.findProperty('loader_version_range') ?: '[4,)'
def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)' def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)'
def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)' def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)'
@@ -71,9 +73,23 @@ 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"
// build.gradle 替换现有 version 逻辑
def gitVersion() { def gitVersion() {
def describe = providers.exec { def describe = providers.exec {
commandLine 'git', 'describe', '--tags', '--always' commandLine 'git', 'describe', '--tags', '--always'
@@ -87,10 +103,10 @@ def gitVersion() {
def count = m.group(2) ? m.group(2) : '0' // tag 后提交数 def count = m.group(2) ? m.group(2) : '0' // tag 后提交数
return "${xy}.${count}-alpha" // 2.2.2-alpha return "${xy}.${count}-alpha" // 2.2.2-alpha
} }
// 兜底(从未打 tag):mod_version + 总提交数 // 兜底(从未打 tag):0.0.<提交总数>-alpha,不依赖写死的 mod_version
def count = providers.exec { def count = providers.exec {
commandLine 'git', 'rev-list', '--count', 'HEAD' commandLine 'git', 'rev-list', '--count', 'HEAD'
}.standardOutput.asText.map { it.trim() }.get() }.standardOutput.asText.map { it.trim() }.get()
return "${mod_version}.${count}-alpha" return "0.0.${count}-alpha"
} }
version = gitVersion() version = modVersion
@@ -87,6 +87,7 @@ public final class RecipeRegistration {
new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.EXCHANGE, 5)); new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.EXCHANGE, 5));
} }
// ==================== 奥术有序配方 ====================
private static void registerShapedArcane() { private static void registerShapedArcane() {
STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_1K), STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_1K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1), new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1),
@@ -214,6 +215,8 @@ public final class RecipeRegistration {
} }
// ==================== 奥术无序配方 ====================
private static void registerShapelessArcane() { private static void registerShapelessArcane() {
// 1. 扩散核心 // 1. 扩散核心
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE), DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
@@ -21,8 +21,11 @@ import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/** /**
* 源质网格服务的实现。查询和提取源质时同时检查两个存储后端: * 源质网格服务的实现。查询和提取源质时同时检查两个存储后端:
* Path A: EssentiaMonitor(注魔供应器/傀儡背包直接写入), * Path A: EssentiaMonitor(历史遗留的内存镜像,现仅参与读取),
* Path B: AE2 原生存储(源质存储元件、输入总线等)。 * Path B: AE2 原生存储(源质存储元件、存储总线等)。
* <p>
* 写入只走 Path B:网络装不下的部分如实返回"未接受",不再回退进内存 monitor
* (否则网络没有源质存储时也会照吸,源质既不在 ME 存储里也不持久化,等于消失)。
*/ */
public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider { public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider {
@@ -101,18 +104,13 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider
@Override @Override
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) { public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
if (amount <= 0) return 0; if (amount <= 0) return 0;
// 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化) // 只承认 AE2 原生存储(源质存储元件 / 存储总线)真正接收的量。
// 网络没有源质存储或已满时如实返回"未接受",调用方(输入总线 / 无线接收器 /
// 源质供应器 / 傀儡背包)据此把源质留在原容器里,不抽走。
// 旧实现把装不下的部分塞进内存 monitor 兜底 —— 等于"网络无存储也照吸",
// 而这些源质不在任何 ME 存储里、不随存档持久化,表现为源质凭空消失。
var nativeInjected = injectNative(aspectId, amount, simulate); var nativeInjected = injectNative(aspectId, amount, simulate);
if (nativeInjected < amount) { return amount - nativeInjected;
// 原生存储空间不足时剩余部分退回内存 monitor(供注魔供应器等内部读取)
long remaining = amount - nativeInjected;
if (!simulate) {
long current = monitor.get(aspectId);
monitor.set(aspectId, current + remaining);
}
return amount - nativeInjected - remaining; // 全部接受(monitor 兜底)
}
return 0; // 全部写入原生存储
} }
private long injectNative(ResourceLocation aspectId, long amount, boolean simulate) { private long injectNative(ResourceLocation aspectId, long amount, boolean simulate) {
@@ -26,42 +26,76 @@ public final class TCReflection {
private TCReflection() {} private TCReflection() {}
/** 从荒古炼金炉内部源质缓存直接抽取(替代蒸馏釜/罐子中转)。 */ /** 读取荒古炼金炉 private aspects 字段(不实现 IAspectContainer;只读,返回活引用用于原地修改)。 */
@Nullable @Nullable
private static AspectStack extractFromFurnace(BlockEntity be, int maxAmount) { private static AspectList getFurnaceAspects(BlockEntity be) {
try { try {
if (furnaceAspectsField == null) { if (furnaceAspectsField == null) {
furnaceAspectsField = be.getClass().getDeclaredField("aspects"); furnaceAspectsField = be.getClass().getDeclaredField("aspects");
furnaceAspectsField.setAccessible(true); furnaceAspectsField.setAccessible(true);
} }
AspectList furnaceAspects = (AspectList) furnaceAspectsField.get(be); Object raw = furnaceAspectsField.get(be);
if (furnaceAspects != null && furnaceAspects.size() > 0) { return raw instanceof AspectList list ? list : null;
Aspect[] aspects = furnaceAspects.getAspects(); } catch (Exception e) {
for (Aspect aspect : aspects) { return null;
if (aspect == null) continue; }
int amt = furnaceAspects.getAmount(aspect); }
if (amt <= 0) continue;
int toExtract = Math.min(amt, maxAmount); /** 荒古炼金炉写回(aspects 为 private 字段,无公开写入 API)。返回实际写入量。 */
furnaceAspects.remove(aspect, toExtract); private static int addToFurnace(BlockEntity be, Aspect aspect, int amount) {
be.setChanged(); if (amount <= 0) return 0;
ResourceLocation id = getAspectId(aspect); try {
return id != null ? new AspectStack(id, toExtract) : null; 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; return null;
} }
@Nullable @Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, int maxAmount) { 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) { if (be == null) {
return null; return null;
} }
// ===== 路径0: 荒古炼金炉直连(AlchemicalFurnaceBlockEntity 不实现 IAspectContainer, // ===== 路径0: 荒古炼金炉直连(AlchemicalFurnaceBlockEntity 不实现 IAspectContainer,
// 源质在 private aspects 缓存——直接抽取,替代"必须经蒸馏釜/罐子")===== // 源质在 private aspects 缓存——直接抽取,替代"必须经蒸馏釜/罐子")=====
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) { if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
AspectStack fromFurnace = extractFromFurnace(be, maxAmount); AspectStack fromFurnace = extractFromFurnace(be, only, maxAmount);
if (fromFurnace != null) return fromFurnace; if (fromFurnace != null) return fromFurnace;
} }
// ===== 路径0b: TC 管道接口(EssentiaTransport——高级荒古炼金炉喷嘴/源质管道)===== // ===== 路径0b: TC 管道接口(EssentiaTransport——高级荒古炼金炉喷嘴/源质管道)=====
@@ -70,14 +104,16 @@ public final class TCReflection {
if (!transport.canOutputTo(face)) continue; if (!transport.canOutputTo(face)) continue;
Aspect aspect = transport.getEssentiaType(face); Aspect aspect = transport.getEssentiaType(face);
if (aspect == null) continue; if (aspect == null) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int amount = transport.getEssentiaAmount(face); int amount = transport.getEssentiaAmount(face);
if (amount <= 0) continue; if (amount <= 0) continue;
int toExtract = Math.min(amount, maxAmount); int toExtract = Math.min(amount, maxAmount);
int taken = transport.takeEssentia(aspect, toExtract, face); int taken = transport.takeEssentia(aspect, toExtract, face);
if (taken > 0) { if (taken > 0) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, taken);
return id != null ? new AspectStack(id, taken) : null;
} }
} }
} }
@@ -94,11 +130,13 @@ public final class TCReflection {
if (aspect == null) continue; if (aspect == null) continue;
int amt = aspectList.getAmount(aspect); int amt = aspectList.getAmount(aspect);
if (amt <= 0) continue; 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); int toExtract = Math.min(amt, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) { if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, toExtract);
return id != null ? new AspectStack(id, toExtract) : null;
} }
} }
} }
@@ -113,11 +151,14 @@ public final class TCReflection {
Aspect aspect = (Aspect) methods[0].invoke(be); Aspect aspect = (Aspect) methods[0].invoke(be);
int amount = (int) methods[1].invoke(be); int amount = (int) methods[1].invoke(be);
if (aspect != null && amount > 0) { 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); int toExtract = Math.min(amount, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) { if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, toExtract);
return id != null ? new AspectStack(id, toExtract) : null;
} }
} }
} catch (Exception ignored) { } catch (Exception ignored) {
@@ -128,19 +169,28 @@ public final class TCReflection {
public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) { public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return; if (be == null || stack.isEmpty()) return;
if (!(be instanceof IAspectContainer container)) return;
Aspect aspect = findAspectById(stack.aspectId()); Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return; 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()); container.addToContainer(aspect, (int) stack.amount());
be.setChanged(); be.setChanged();
} }
public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) { public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return 0; if (be == null || stack.isEmpty()) return 0;
if (!(be instanceof IAspectContainer container)) return 0;
Aspect aspect = findAspectById(stack.aspectId()); Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return 0; 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 leftover = container.addToContainer(aspect, toInsert);
int added = toInsert - leftover; int added = toInsert - leftover;
if (added > 0) { if (added > 0) {
@@ -149,6 +199,61 @@ public final class TCReflection {
return added; 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 @Nullable
public static Aspect findAspectById(ResourceLocation id) { public static Aspect findAspectById(ResourceLocation id) {
if (id == null) return null; if (id == null) return null;
@@ -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 IDLE_POWER = 0.5;
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA; 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; private boolean isWorkingFlag = false;
/**
* 待注入 AE 网络的源质缓冲(随总线 NBT 持久化)。
* <p>
* 网络装不下(无源质存储 / 已满 / 模拟与实插不一致)时源质留在这里重试,
* 绝不丢弃;缓冲非空期间也不再从容器抽取,避免"吸走却存不下"。
*/
private AspectStack buffer = AspectStack.EMPTY;
/** /**
* 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。 * 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。 * 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) { protected boolean doBusWork(IGrid grid) {
this.isWorkingFlag = false; this.isWorkingFlag = false;
// 1) 先冲缓冲:网络有空间时把上次暂存的源质注入
boolean worked = flushBuffer(grid);
Direction side = this.getSide(); Direction side = this.getSide();
if (side == null) return false; if (side == null) return worked;
BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side); BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side);
Level levelRaw = this.getLevel(); Level levelRaw = this.getLevel();
if (!(levelRaw instanceof ServerLevel level)) return false; if (!(levelRaw instanceof ServerLevel level)) return worked;
if (!level.isLoaded(adjacentPos)) return false; if (!level.isLoaded(adjacentPos)) return worked;
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos); BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false; if (adjacentBE == null) return worked;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, getTransferRate()); // 2) 缓冲仍有剩余 = 网络装不下 → 不再从容器抽取(源质留在原容器里)
if (extracted == null || extracted.isEmpty()) return false; 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); long injected = this.injectToNetwork(grid, extracted);
if (injected <= 0) { if (injected < extracted.amount()) {
TCReflection.returnEssentia(adjacentBE, extracted); // 模拟与实插不一致(例如挂载的容器存储无法模拟)→ 剩余留在缓冲里重试,绝不归还/丢弃
return false; this.buffer = new AspectStack(extracted.aspectId(), extracted.amount() - injected);
markForSave();
} }
double powerCost = injected * TRANSFER_POWER; double powerCost = injected * TRANSFER_POWER;
grid.getEnergyService().extractAEPower(powerCost, Actionable.MODULATE, PowerMultiplier.CONFIG); grid.getEnergyService().extractAEPower(powerCost, Actionable.MODULATE, PowerMultiplier.CONFIG);
if (injected < extracted.amount()) { this.isWorkingFlag = injected > 0 || worked;
long leftover = extracted.amount() - injected; return injected > 0 || worked;
TCReflection.returnEssentia(adjacentBE, new AspectStack(extracted.aspectId(), leftover)); }
}
/** 把缓冲里的源质注入网络。@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; this.isWorkingFlag = true;
return true; return true;
} }
private void markForSave() {
if (getHost() != null) {
getHost().markForSave();
}
}
private long injectToNetwork(IGrid grid, AspectStack stack) { private long injectToNetwork(IGrid grid, AspectStack stack) {
AEssentiaKey key = AEssentiaKey.of(stack.aspectId()); AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
if (key == null) return 0;
return grid.getStorageService().getInventory().insert( return grid.getStorageService().getInventory().insert(
key, stack.amount(), Actionable.MODULATE, key, stack.amount(), Actionable.MODULATE,
IActionSource.ofMachine(this)); IActionSource.ofMachine(this));
@@ -143,12 +189,23 @@ public class EssentiaImportBusPart extends IOBusPart
public void readFromNBT(CompoundTag extra, HolderLookup.Provider registries) { public void readFromNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.readFromNBT(extra, registries); super.readFromNBT(extra, registries);
this.keyTypeSelection.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 @Override
public void writeToNBT(CompoundTag extra, HolderLookup.Provider registries) { public void writeToNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.writeToNBT(extra, registries); super.writeToNBT(extra, registries);
this.keyTypeSelection.writeToNBT(extra); 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 @Override
@@ -164,3 +221,4 @@ public class EssentiaImportBusPart extends IOBusPart
return this.keyTypeSelection; return this.keyTypeSelection;
} }
} }
␍
@@ -1,11 +1,14 @@
package thaumicenergistics.common.parts; package thaumicenergistics.common.parts;
import appeng.api.config.Actionable; import appeng.api.AECapabilities;
import appeng.api.networking.IGrid; import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.security.IActionSource; import appeng.api.networking.security.IActionSource;
import appeng.api.parts.IPartCollisionHelper; import appeng.api.parts.IPartCollisionHelper;
import appeng.api.parts.IPartItem; import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel; 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.KeyTypeSelection;
import appeng.api.util.KeyTypeSelectionHost; import appeng.api.util.KeyTypeSelectionHost;
import appeng.core.settings.TickRates; import appeng.core.settings.TickRates;
@@ -16,22 +19,30 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType; import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
import java.util.Set; import java.util.Set;
/**
* 源质存储总线:把相邻的源质容器(要素罐子 / 奥术蒸馏器 / 荒古炼金炉 / 源质管道)
* 或相邻方块自身暴露的 {@code MEStorage}(例如子网接口)挂载为网络存储,
* 使终端与子网能读写该容器。
* <p>
* 实现方式与 AE2 原版存储总线一致({@link IStorageProvider#mountInventories}),
* 不主动搬运源质。旧实现按输入总线逻辑把容器抽干再插入网络、且忽略插入返回值,
* 导致网络塞不下时源质凭空消失——已移除。
*/
public class EssentiaStorageBusPart extends IOBusPart public class EssentiaStorageBusPart extends IOBusPart
implements KeyTypeSelectionHost { implements IStorageProvider, KeyTypeSelectionHost {
@PartModels @PartModels
public static final ResourceLocation RL_BASE = 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); public static final PartModel MODEL_ON = new PartModel(RL_BASE, RL_ON);
private static final double IDLE_POWER = 1.0; private static final double IDLE_POWER = 1.0;
private static final int TRANSFER_RATE = 8;
private boolean isWorkingFlag = false; private boolean isWorkingFlag = false;
/** 相邻源质容器的 MEStorage 视图(惰性创建,内部实时解析方块实体)。 */
private EssentiaContainerStorage containerStorage;
private Component storageDescription;
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection( private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
selection -> { }, selection -> { },
keyType -> keyType == AEssentiaKeyType.INSTANCE); keyType -> keyType == AEssentiaKeyType.INSTANCE);
@@ -61,6 +75,8 @@ public class EssentiaStorageBusPart extends IOBusPart
public EssentiaStorageBusPart(IPartItem<?> partItem) { public EssentiaStorageBusPart(IPartItem<?> partItem) {
super(TickRates.StorageBus, Set.of(AEssentiaKeyType.INSTANCE), partItem); super(TickRates.StorageBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
this.getMainNode().setIdlePowerUsage(IDLE_POWER); this.getMainNode().setIdlePowerUsage(IDLE_POWER);
// 注册为存储提供者,网络挂载时会调用 mountInventories(与 AE2 原版存储总线一致)
this.getMainNode().addService(IStorageProvider.class, this);
} }
@Override @Override
@@ -77,32 +93,81 @@ public class EssentiaStorageBusPart extends IOBusPart
return MODEL_OFF; return MODEL_OFF;
} }
/** 存储总线不做搬运工作:容器由 {@link #mountInventories} 挂载为网络存储。
* 这里只做状态指示(相邻容器有源质 → 亮工作灯)。 */
@Override @Override
protected boolean doBusWork(IGrid grid) { protected boolean doBusWork(appeng.api.networking.IGrid grid) {
this.isWorkingFlag = false; BlockEntity adjacent = getAdjacentBlockEntity();
this.isWorkingFlag = adjacent != null
Direction side = this.getSide(); && thaumicenergistics.common.integration.tc.TCReflection.peekEssentia(adjacent, 1) != null;
if (side == null) return false; 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;
} }
private void insertIntoNetwork(IGrid grid, AspectStack stack) { // ===== IStorageProvider:把相邻存储挂载到网络 =====
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
grid.getStorageService().getInventory().insert( @Override
key, stack.amount(), Actionable.MODULATE, public void mountInventories(IStorageMounts mounts) {
IActionSource.ofMachine(this)); 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 @Override
@@ -7,6 +7,7 @@ import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaTransport; import thaumcraft.api.aspects.IEssentiaTransport;
import thaumcraft.common.essentia.EssentiaTransport; import thaumcraft.common.essentia.EssentiaTransport;
import thaumicenergistics.init.ModBlockEntities; import thaumicenergistics.init.ModBlockEntities;
@@ -24,6 +25,17 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
/** 接收器每次输入/输出源质上限。 */ /** 接收器每次输入/输出源质上限。 */
public static final int TRANSFER_LIMIT = 16; 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 linkCheckCounter = 0;
private int pullCounter = 0; private int pullCounter = 0;
private static final int PULL_INTERVAL = 10; private static final int PULL_INTERVAL = 10;
@@ -39,15 +51,48 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
} }
if (++be.pullCounter >= PULL_INTERVAL) { if (++be.pullCounter >= PULL_INTERVAL) {
be.pullCounter = 0; be.pullCounter = 0;
be.takeEssentiaFromNeighbors(); 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 网络。 */ /** 模拟管道:沿 6 个方向扫描,最近的有效源质源(蒸馏器/罐/管道)隔空拉取,经连接供应器注入 AE 网络。 */
private void takeEssentiaFromNeighbors() { private void takeEssentiaFromNeighbors() {
if (level == null || level.isClientSide()) return; if (level == null || level.isClientSide()) return;
TileEssentiaProvider provider = getLinkedProvider(); TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return; if (provider == null || !provider.thE_isActive()) return;
// 缓冲还没清空 = 网络装不下,先别再吸(否则"吸走却存不下")
if (!buffer.isEmpty()) return;
for (Direction side : Direction.values()) { for (Direction side : Direction.values()) {
if (!canInputFrom(side)) continue; if (!canInputFrom(side)) continue;
// 沿方向行走:非传输方块 = 管道断;空管道 = 继续;有源质 = 拉取 // 沿方向行走:非传输方块 = 管道断;空管道 = 继续;有源质 = 拉取
@@ -68,11 +113,15 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
} }
Aspect sourceAspect = source.getEssentiaType(sourceSide); Aspect sourceAspect = source.getEssentiaType(sourceSide);
if (sourceAspect == null) break; if (sourceAspect == null) break;
// 直接拉取并注入(injectEssentia 返回 0 = 全部接受,无需模拟预检) // 预检:网络能收多少(rejected = 收不下的量);一点都收不下就完全不抽取
int want = Math.min(Math.max(srcAmount, 1), TRANSFER_LIMIT); int want = Math.min(Math.max(srcAmount, 1), TRANSFER_LIMIT);
int taken = source.takeEssentia(sourceAspect, want, sourceSide); 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) { if (taken > 0) {
provider.thE_inject(sourceAspect, taken, false); buffer.add(sourceAspect, taken); // 先进缓冲,注入失败也不丢
flushBuffer();
} }
break; // 拉到即停 break; // 拉到即停
} }
@@ -194,10 +243,18 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
@Override @Override
public int addEssentia(Aspect aspect, int amount, Direction side) { public int addEssentia(Aspect aspect, int amount, Direction side) {
// 无线输入:转发到连接供应器注入 AE 网络,每次上限 16 // 无线输入:先问网络能收多少,只接收装得下的部分(其余留在管道/容器里,绝不吞掉)。
// 接收后先进缓冲,注入失败下次重试——保证源质不丢。
TileEssentiaProvider provider = getLinkedProvider(); TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0; if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0;
return (int) provider.thE_inject(aspect, Math.min(amount, TRANSFER_LIMIT), false); 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 @Override
@@ -248,6 +305,9 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
if (linkedProviderPos != null) { if (linkedProviderPos != null) {
tag.putLong("LinkedProvider", linkedProviderPos.asLong()); tag.putLong("LinkedProvider", linkedProviderPos.asLong());
} }
if (!buffer.isEmpty()) {
buffer.writeToNBT(tag, NBT_BUFFER);
}
} }
@Override @Override
@@ -258,5 +318,9 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
} else { } else {
linkedProviderPos = null; linkedProviderPos = null;
} }
buffer.aspects.clear();
if (tag.contains(NBT_BUFFER)) {
buffer.readFromNBT(tag, NBT_BUFFER);
}
} }
} }
@@ -54,7 +54,11 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
/** 手册槽位。 */ /** 手册槽位。 */
public static final int BOOK_SLOT = 0; public static final int BOOK_SLOT = 0;
private static final int SCAN_INTERVAL = 10; // 每 10 tick 扫描一次祭坛 private static final int SCAN_INTERVAL = 10; // 每 10 tick 验证一次祭坛(缓存命中时仅 1 次查询)
/** 未找到祭坛时的扫描退避间隔(tick):全量扫描约 9800 次 getBlockEntity,附近无祭坛时降低频率控 mspt。 */
private static final int SCAN_MISS_INTERVAL = 200;
/** 风险重算限频(tick):computeRisk 内部也要扫上千次 getBlockEntity(配方估算/源质汇总),控制 mspt。 */
private static final int RISK_INTERVAL = 40;
protected final IManagedGridNode mainNode; protected final IManagedGridNode mainNode;
private final SimpleContainer inventory = new SimpleContainer(1) { private final SimpleContainer inventory = new SimpleContainer(1) {
@@ -67,7 +71,16 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
}; };
private int scanCounter = 0; private int scanCounter = 0;
private int riskCounter = 0;
private BlockPos matrixPos = null; 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; private RiskData cachedRisk = RiskData.NONE;
@@ -112,6 +125,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override @Override
public void onLoad() { public void onLoad() {
super.onLoad(); super.onLoad();
unloading = false;
if (level != null && !level.isClientSide()) { if (level != null && !level.isClientSide()) {
mainNode.create(level, getBlockPos()); mainNode.create(level, getBlockPos());
} }
@@ -121,6 +135,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override @Override
public void setRemoved() { public void setRemoved() {
unloading = true; // 先置标志再销毁网格节点:节点销毁回调会触发 updateNetworkState
super.setRemoved(); super.setRemoved();
if (level != null && !level.isClientSide()) { if (level != null && !level.isClientSide()) {
discardBubble(); discardBubble();
@@ -132,6 +147,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
@Override @Override
public void onChunkUnloaded() { public void onChunkUnloaded() {
unloading = true; // 同上:卸载期间禁止改方块状态,否则 chunk 被重新标记脏 → 保存循环
super.onChunkUnloaded(); super.onChunkUnloaded();
if (mainNode != null) { if (mainNode != null) {
mainNode.destroy(); mainNode.destroy();
@@ -151,7 +167,10 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) { public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) {
if (level.isClientSide()) return; if (level.isClientSide()) return;
if (++be.scanCounter >= SCAN_INTERVAL) { // 无祭坛缓存时全量扫描退避到 SCAN_MISS_INTERVAL(否则每 10 tick 扫 9800 次 getBlockEntity,
// 附近没建祭坛时也持续 ~70us/t)
int interval = be.matrixPos == null ? SCAN_MISS_INTERVAL : SCAN_INTERVAL;
if (++be.scanCounter >= interval) {
be.scanCounter = 0; be.scanCounter = 0;
be.scanMatrix(); be.scanMatrix();
} }
@@ -159,16 +178,50 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
be.updateBubble(); be.updateBubble();
} }
/** 扫描附近注魔矩阵并缓存风险数据。 */ /** 扫描附近注魔矩阵并缓存风险数据。
* 快速路径:已缓存矩阵仍在 → 只做 1 次 getBlockEntity 验证;
* 全量扫描(33×9×33≈9800 次 getBlockEntity)仅缓存失效时执行,是 mspt 大户。
* 风险重算限频(配方估算/源质汇总还要上千次查询),但注魔开始/结束瞬间(crafting 翻转)强制立即重算。 */
private void scanMatrix() { private void scanMatrix() {
BlockPos found = null; BlockPos found = matrixPos;
InfusionRiskCalculator.InfusionMatrixInfo info = InfusionRiskCalculator.scanNearbyMatrix(level, worldPosition); boolean craftingChanged = false;
RiskData newRisk = info != null ? InfusionRiskCalculator.computeRisk(level, info) : RiskData.NONE; // 快速路径:缓存矩阵仍在 → 跳过全量扫描
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); boolean riskChanged = !newRisk.equals(this.cachedRisk);
this.cachedRisk = newRisk; this.cachedRisk = newRisk;
if (info != null) {
found = info.matrixPos();
}
if (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) { if (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) {
this.matrixPos = found; this.matrixPos = found;
markForUpdate(); markForUpdate();
@@ -178,7 +231,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
// ===== 网格节点 / 状态同步 ===== // ===== 网格节点 / 状态同步 =====
private void updateNetworkState() { private void updateNetworkState() {
if (level == null || level.isClientSide()) return; if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean net = mainNode.isActive(); boolean net = mainNode.isActive();
BlockState st = getBlockState(); BlockState st = getBlockState();
if (st.getValue(ModBlocks.NETWORK) != net) { if (st.getValue(ModBlocks.NETWORK) != net) {
@@ -192,7 +245,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
} }
private void updateBookState() { private void updateBookState() {
if (level == null || level.isClientSide()) return; if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty(); boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty();
BlockState st = getBlockState(); BlockState st = getBlockState();
if (st.getValue(ModBlocks.BOOK) != hasBook) { if (st.getValue(ModBlocks.BOOK) != hasBook) {
@@ -211,17 +264,23 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
* 满足 → 生成/更新气泡;不满足 → 销毁气泡。 * 满足 → 生成/更新气泡;不满足 → 销毁气泡。
*/ */
private void updateBubble() { private void updateBubble() {
if (level == null || level.isClientSide()) return; if (level == null || level.isClientSide() || unloading) return;
boolean shouldShow = getBlockState().getValue(ModBlocks.BOOK) boolean shouldShow = getBlockState().getValue(ModBlocks.BOOK)
&& getBlockState().getValue(ModBlocks.NETWORK) && getBlockState().getValue(ModBlocks.NETWORK)
&& cachedRisk.found(); && cachedRisk.found();
if (shouldShow) { if (shouldShow) {
ensureBubble(); ensureBubble();
if (bubble != null) { if (bubble != null) {
applyBubbleNbt(buildBubbleText(cachedRisk)); // 文本未变则跳过反射/NBT 重建(原实现每 tick 都重建,是 mspt 开销之一)
net.minecraft.network.chat.Component text = buildBubbleText(cachedRisk);
if (!text.equals(lastBubbleText)) {
lastBubbleText = text;
applyBubbleNbt(text);
}
} }
} else { } else {
discardBubble(); discardBubble();
lastBubbleText = null;
} }
} }
@@ -459,12 +518,12 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
} }
public void markForUpdate() { public void markForUpdate() {
if (level != null) { // 卸载/移除中跳过:sendBlockUpdated / blockEntityChanged 会把区块重新标记脏,停服保存时形成死循环
setChanged(); if (level == null || unloading || isRemoved()) return;
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); setChanged();
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖) level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
level.blockEntityChanged(worldPosition); // blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
} level.blockEntityChanged(worldPosition);
} }
// ===== NBT ===== // ===== NBT =====