v2.3.14
@@ -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,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 describe = providers.exec {
|
||||
commandLine 'git', 'describe', '--tags', '--always'
|
||||
@@ -87,10 +103,10 @@ def gitVersion() {
|
||||
def count = m.group(2) ? m.group(2) : '0' // tag 后提交数
|
||||
return "${xy}.${count}-alpha" // 2.2.2-alpha
|
||||
}
|
||||
// 兜底(从未打 tag):mod_version + 总提交数
|
||||
// 兜底(从未打 tag):0.0.<提交总数>-alpha,不依赖写死的 mod_version
|
||||
def count = providers.exec {
|
||||
commandLine 'git', 'rev-list', '--count', 'HEAD'
|
||||
}.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));
|
||||
}
|
||||
|
||||
// ==================== 奥术有序配方 ====================
|
||||
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),
|
||||
@@ -214,6 +215,8 @@ public final class RecipeRegistration {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================== 奥术无序配方 ====================
|
||||
private static void registerShapelessArcane() {
|
||||
// 1. 扩散核心
|
||||
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
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);
|
||||
}
|
||||
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();
|
||||
ResourceLocation id = getAspectId(aspect);
|
||||
return id != null ? new AspectStack(id, toExtract) : null;
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
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,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
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -24,6 +25,17 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
/** 接收器每次输入/输出源质上限。 */
|
||||
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;
|
||||
@@ -39,15 +51,48 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
}
|
||||
if (++be.pullCounter >= PULL_INTERVAL) {
|
||||
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 网络。 */
|
||||
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;
|
||||
// 沿方向行走:非传输方块 = 管道断;空管道 = 继续;有源质 = 拉取
|
||||
@@ -68,11 +113,15 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
}
|
||||
Aspect sourceAspect = source.getEssentiaType(sourceSide);
|
||||
if (sourceAspect == null) break;
|
||||
// 直接拉取并注入(injectEssentia 返回 0 = 全部接受,无需模拟预检)
|
||||
// 预检:网络能收多少(rejected = 收不下的量);一点都收不下就完全不抽取
|
||||
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) {
|
||||
provider.thE_inject(sourceAspect, taken, false);
|
||||
buffer.add(sourceAspect, taken); // 先进缓冲,注入失败也不丢
|
||||
flushBuffer();
|
||||
}
|
||||
break; // 拉到即停
|
||||
}
|
||||
@@ -194,10 +243,18 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
|
||||
@Override
|
||||
public int addEssentia(Aspect aspect, int amount, Direction side) {
|
||||
// 无线输入:转发到连接供应器注入 AE 网络,每次上限 16
|
||||
// 无线输入:先问网络能收多少,只接收装得下的部分(其余留在管道/容器里,绝不吞掉)。
|
||||
// 接收后先进缓冲,注入失败下次重试——保证源质不丢。
|
||||
TileEssentiaProvider provider = getLinkedProvider();
|
||||
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
|
||||
@@ -248,6 +305,9 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
if (linkedProviderPos != null) {
|
||||
tag.putLong("LinkedProvider", linkedProviderPos.asLong());
|
||||
}
|
||||
if (!buffer.isEmpty()) {
|
||||
buffer.writeToNBT(tag, NBT_BUFFER);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -258,5 +318,9 @@ public class TileEssentiaProviderConnection extends BlockEntity implements IEsse
|
||||
} else {
|
||||
linkedProviderPos = null;
|
||||
}
|
||||
buffer.aspects.clear();
|
||||
if (tag.contains(NBT_BUFFER)) {
|
||||
buffer.readFromNBT(tag, NBT_BUFFER);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,11 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
|
||||
/** 手册槽位。 */
|
||||
public static final int BOOK_SLOT = 0;
|
||||
private static final int SCAN_INTERVAL = 10; // 每 10 tick 扫描一次祭坛
|
||||
private static final int SCAN_INTERVAL = 10; // 每 10 tick 验证一次祭坛(缓存命中时仅 1 次查询)
|
||||
/** 未找到祭坛时的扫描退避间隔(tick):全量扫描约 9800 次 getBlockEntity,附近无祭坛时降低频率控 mspt。 */
|
||||
private static final int SCAN_MISS_INTERVAL = 200;
|
||||
/** 风险重算限频(tick):computeRisk 内部也要扫上千次 getBlockEntity(配方估算/源质汇总),控制 mspt。 */
|
||||
private static final int RISK_INTERVAL = 40;
|
||||
|
||||
protected final IManagedGridNode mainNode;
|
||||
private final SimpleContainer inventory = new SimpleContainer(1) {
|
||||
@@ -67,7 +71,16 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
};
|
||||
|
||||
private int scanCounter = 0;
|
||||
private int riskCounter = 0;
|
||||
private BlockPos matrixPos = null;
|
||||
/** 上次扫描到的祭坛注魔中状态(翻转时强制立即重算风险,抓住注魔启动瞬间的高不稳定度)。 */
|
||||
private boolean cachedCrafting = false;
|
||||
|
||||
/** 区块卸载/方块移除中:禁止 setBlock / markForUpdate(卸载时改方块状态会触发区块重载,停服保存死循环)。 */
|
||||
private boolean unloading = false;
|
||||
|
||||
/** 上一次写入气泡的文本(内容未变则跳过反射/NBT 重建,避免每 tick 开销)。 */
|
||||
private net.minecraft.network.chat.Component lastBubbleText = null;
|
||||
|
||||
/** 缓存的注魔风险数据(弹窗显示)。 */
|
||||
private RiskData cachedRisk = RiskData.NONE;
|
||||
@@ -112,6 +125,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
@Override
|
||||
public void onLoad() {
|
||||
super.onLoad();
|
||||
unloading = false;
|
||||
if (level != null && !level.isClientSide()) {
|
||||
mainNode.create(level, getBlockPos());
|
||||
}
|
||||
@@ -121,6 +135,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
|
||||
@Override
|
||||
public void setRemoved() {
|
||||
unloading = true; // 先置标志再销毁网格节点:节点销毁回调会触发 updateNetworkState
|
||||
super.setRemoved();
|
||||
if (level != null && !level.isClientSide()) {
|
||||
discardBubble();
|
||||
@@ -132,6 +147,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
|
||||
@Override
|
||||
public void onChunkUnloaded() {
|
||||
unloading = true; // 同上:卸载期间禁止改方块状态,否则 chunk 被重新标记脏 → 保存循环
|
||||
super.onChunkUnloaded();
|
||||
if (mainNode != null) {
|
||||
mainNode.destroy();
|
||||
@@ -151,7 +167,10 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
|
||||
public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) {
|
||||
if (level.isClientSide()) return;
|
||||
if (++be.scanCounter >= SCAN_INTERVAL) {
|
||||
// 无祭坛缓存时全量扫描退避到 SCAN_MISS_INTERVAL(否则每 10 tick 扫 9800 次 getBlockEntity,
|
||||
// 附近没建祭坛时也持续 ~70us/t)
|
||||
int interval = be.matrixPos == null ? SCAN_MISS_INTERVAL : SCAN_INTERVAL;
|
||||
if (++be.scanCounter >= interval) {
|
||||
be.scanCounter = 0;
|
||||
be.scanMatrix();
|
||||
}
|
||||
@@ -159,16 +178,50 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
be.updateBubble();
|
||||
}
|
||||
|
||||
/** 扫描附近注魔矩阵并缓存风险数据。 */
|
||||
/** 扫描附近注魔矩阵并缓存风险数据。
|
||||
* 快速路径:已缓存矩阵仍在 → 只做 1 次 getBlockEntity 验证;
|
||||
* 全量扫描(33×9×33≈9800 次 getBlockEntity)仅缓存失效时执行,是 mspt 大户。
|
||||
* 风险重算限频(配方估算/源质汇总还要上千次查询),但注魔开始/结束瞬间(crafting 翻转)强制立即重算。 */
|
||||
private void scanMatrix() {
|
||||
BlockPos found = null;
|
||||
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);
|
||||
RiskData newRisk = info != null ? InfusionRiskCalculator.computeRisk(level, info) : RiskData.NONE;
|
||||
found = info != null ? info.matrixPos() : null;
|
||||
}
|
||||
|
||||
RiskData newRisk = cachedRisk;
|
||||
if (found != null) {
|
||||
// 按 tick 累计(每轮扫描间隔 SCAN_INTERVAL);首次发现即时重算(覆盖层/气泡 0.5 秒内出现),
|
||||
// 之后每 RISK_INTERVAL tick 重算一次(按"扫描轮数"计数会变成 40 轮×10 tick=20 秒才出数据);
|
||||
// 注魔开始/结束瞬间(crafting 翻转)也立即重算,抓住启动时的高不稳定度。
|
||||
riskCounter += SCAN_INTERVAL;
|
||||
if (riskCounter >= RISK_INTERVAL || !cachedRisk.found() || craftingChanged) {
|
||||
riskCounter = 0;
|
||||
if (level.getBlockEntity(found) instanceof thaumcraft.common.blockentities.InfusionMatrixBlockEntity matrix) {
|
||||
newRisk = InfusionRiskCalculator.computeRisk(level,
|
||||
new InfusionRiskCalculator.InfusionMatrixInfo(found, matrix));
|
||||
} else {
|
||||
newRisk = RiskData.NONE;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newRisk = RiskData.NONE;
|
||||
cachedCrafting = false;
|
||||
}
|
||||
|
||||
boolean riskChanged = !newRisk.equals(this.cachedRisk);
|
||||
this.cachedRisk = newRisk;
|
||||
if (info != null) {
|
||||
found = info.matrixPos();
|
||||
}
|
||||
if (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) {
|
||||
this.matrixPos = found;
|
||||
markForUpdate();
|
||||
@@ -178,7 +231,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
// ===== 网格节点 / 状态同步 =====
|
||||
|
||||
private void updateNetworkState() {
|
||||
if (level == null || level.isClientSide()) return;
|
||||
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
|
||||
boolean net = mainNode.isActive();
|
||||
BlockState st = getBlockState();
|
||||
if (st.getValue(ModBlocks.NETWORK) != net) {
|
||||
@@ -192,7 +245,7 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
}
|
||||
|
||||
private void updateBookState() {
|
||||
if (level == null || level.isClientSide()) return;
|
||||
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
|
||||
boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty();
|
||||
BlockState st = getBlockState();
|
||||
if (st.getValue(ModBlocks.BOOK) != hasBook) {
|
||||
@@ -211,17 +264,23 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
* 满足 → 生成/更新气泡;不满足 → 销毁气泡。
|
||||
*/
|
||||
private void updateBubble() {
|
||||
if (level == null || level.isClientSide()) return;
|
||||
if (level == null || level.isClientSide() || unloading) return;
|
||||
boolean shouldShow = getBlockState().getValue(ModBlocks.BOOK)
|
||||
&& getBlockState().getValue(ModBlocks.NETWORK)
|
||||
&& cachedRisk.found();
|
||||
if (shouldShow) {
|
||||
ensureBubble();
|
||||
if (bubble != null) {
|
||||
applyBubbleNbt(buildBubbleText(cachedRisk));
|
||||
// 文本未变则跳过反射/NBT 重建(原实现每 tick 都重建,是 mspt 开销之一)
|
||||
net.minecraft.network.chat.Component text = buildBubbleText(cachedRisk);
|
||||
if (!text.equals(lastBubbleText)) {
|
||||
lastBubbleText = text;
|
||||
applyBubbleNbt(text);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
discardBubble();
|
||||
lastBubbleText = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -459,13 +518,13 @@ public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNode
|
||||
}
|
||||
|
||||
public void markForUpdate() {
|
||||
if (level != null) {
|
||||
// 卸载/移除中跳过:sendBlockUpdated / blockEntityChanged 会把区块重新标记脏,停服保存时形成死循环
|
||||
if (level == null || unloading || isRemoved()) return;
|
||||
setChanged();
|
||||
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
|
||||
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
|
||||
level.blockEntityChanged(worldPosition);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== NBT =====
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |