[释放]修正研究节点未解锁时可以使用已有合成的漏洞;修复要素中继接口状态栏;删除源质发信器多余渲染。

This commit is contained in:
1820982382
2026-08-08 13:00:16 +08:00
parent 3573873518
commit 2d93c9d256
30 changed files with 415 additions and 22 deletions
+4 -5
View File
@@ -4,15 +4,14 @@ run/
library/ library/
.idea/ .idea/
.vscode/ .vscode/
.reasonix/
.superpowers/ .superpowers/
.workbuddy/ .workbuddy/
.trae/
.iml .iml
*.log *.log
task_plan.md task_plan.md
findings.md findings.md
progress.md progress.md
library/参考/ /modules/thaumicenergistics-neo/存储体系方案.md
library/log/ /modules/thaumicenergistics-neo/计划书-配方溯源.md
library/*.md
library/*.txt
路径.md
+14 -1
View File
@@ -1,5 +1,5 @@
# Project # Project
mod_version=2.1.0 mod_version=2.0.1
mod_group=thaumicenergistics mod_group=thaumicenergistics
# Mod # Mod
@@ -16,3 +16,16 @@ ae2_version=19.2.17
jei_version=19.39.0.368 jei_version=19.39.0.368
thaumcraft_version=0.2.2.34-port.1 thaumcraft_version=0.2.2.34-port.1
jade_version=15.9.1+neoforge jade_version=15.9.1+neoforge
org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
minecraft_version_range=[1.21.1,1.22)
neoforge_version_range=[21,)
loader_version_range=[4,)
systemProp.http.proxyHost=127.0.0.1
systemProp.http.proxyPort=7890
systemProp.https.proxyHost=127.0.0.1
systemProp.https.proxyPort=7890
systemProp.http.nonProxyHosts=localhost|127.*
Binary file not shown.
+2 -3
View File
@@ -1,8 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+1 -6
View File
@@ -6,9 +6,4 @@ pluginManagement {
} }
} }
plugins { rootProject.name = "thaumicenergistics-neo"
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
include("thaumicenergistics")
project(":thaumicenergistics").projectDir = file("modules/thaumicenergistics-neo")
@@ -0,0 +1,94 @@
package thaumicenergistics.client;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.ScreenEvent;
import appeng.api.networking.crafting.ICraftingSubmitResult;
import appeng.client.gui.me.crafting.CraftConfirmScreen;
import appeng.client.gui.me.crafting.CraftErrorScreen;
import appeng.menu.me.crafting.CraftConfirmMenu;
import thaumicenergistics.ThaumicEnergistics;
/**
* 客户端:研究未解锁拦截的提示渲染。
*
* 服务端拦截合成提交后,通过 ResearchGateErrorPacket 发送研究键;
* 本类在合成屏(确认屏/错误屏)之上绘制提示文字(最上层,不被 GUI 遮挡),
* 并暴露 hasResearchGateError() 供 MixinCraftConfirmScreen 判断是否拦截 CPU 忙错误屏。
*/
@EventBusSubscriber(modid = ThaumicEnergistics.MODID, value = Dist.CLIENT)
public final class ResearchGateOverlay {
private static String pendingResearch = null;
private ResearchGateOverlay() {}
/** 网络包处理:记录研究键(客户端线程)。 */
public static void setPendingResearch(String research) {
pendingResearch = research;
}
/**
* 当前菜单是否处于研究未解锁拦截状态。
* 依据 CraftConfirmMenu.submitError 判断:错误结果存在且 errorCode 非空,
* 说明本次提交被拒绝(研究拦截触发 CPU_BUSY)。
*/
public static boolean hasResearchGateError() {
return pendingResearch != null;
}
/** 清空研究拦截状态(玩家离开合成屏或成功重试时由外部调用)。 */
public static void clearPendingResearch() {
pendingResearch = null;
}
@SubscribeEvent
public static void onScreenRenderPost(ScreenEvent.Render.Post event) {
// 玩家已离开合成屏(不是确认屏/错误屏)→ 清除拦截状态
if (!(event.getScreen() instanceof CraftConfirmScreen)
&& !(event.getScreen() instanceof CraftErrorScreen)) {
if (pendingResearch != null) {
clearPendingResearch();
}
return;
}
// 无拦截状态不渲染
if (pendingResearch == null) {
return;
}
GuiGraphics gui = event.getGuiGraphics();
Minecraft mc = Minecraft.getInstance();
int width = mc.getWindow().getGuiScaledWidth();
// 纯色无样式,保证与 MC 默认 GUI 字体一致
Component msg = Component.literal("当前研究节点未解锁:" + pendingResearch);
int textWidth = mc.font.width(msg);
int x = (width - textWidth) / 2;
int y = 30;
drawWithShadow(gui, mc, msg, x, y, 0xFF5555, 0x000000);
}
/** 以指定颜色绘制文字并叠 8 向同偏移描边,保证在全屏 GUI 之上可读。 */
private static void drawWithShadow(GuiGraphics gui, Minecraft mc, Component msg, int x, int y,
int textColor, int outlineColor) {
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) {
continue;
}
gui.drawString(mc.font, msg, x + dx, y + dy, outlineColor);
}
}
gui.drawString(mc.font, msg, x, y, textColor);
}
}
@@ -4,8 +4,10 @@ import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.AEItemKey; import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack; import appeng.api.stacks.GenericStack;
import appeng.core.definitions.AEItems; import appeng.core.definitions.AEItems;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
@@ -13,11 +15,13 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType; import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.Slot; import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import net.neoforged.neoforge.items.ItemStackHandler; import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler; import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.ThaumcraftApiHelper; import thaumcraft.api.ThaumcraftApiHelper;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList; import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.research.TCResearchManager;
import thaumcraft.common.research.ThaumometerScanManager; import thaumcraft.common.research.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey; import thaumicenergistics.common.integration.appeng.AEssentiaKey;
@@ -31,6 +35,10 @@ import java.util.List;
public class ContainerDistillationPatternEncoder extends AbstractContainerMenu { public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
/** 蒸馏编码统一要求的研究节点(要素蒸馏)。 */
private static final String RESEARCH_DISTILESSENTIA = "DISTILESSENTIA";
private static final String NBTKEY_RESEARCH = "research";
private static final int PLAYER_INV_Y = 150; private static final int PLAYER_INV_Y = 150;
private static final int HOTBAR_Y = 208; private static final int HOTBAR_Y = 208;
private static final int PLAYER_INV_X = 8; private static final int PLAYER_INV_X = 8;
@@ -249,6 +257,13 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
public void onEncodePattern() { public void onEncodePattern() {
if (encoder == null) return; if (encoder == null) return;
// 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板
if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) {
ThaumicEnergistics.LOG.info("[DistillationEncoder] 玩家未解锁研究 {},拒绝编码蒸馏样板", RESEARCH_DISTILESSENTIA);
return;
}
Aspect selected = getSelectedAspect(); Aspect selected = getSelectedAspect();
ItemStack source = getSourceItem(); ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS); ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
@@ -288,6 +303,11 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
List.of(output) List.of(output)
); );
// 在样板上标记研究键(供使用端按玩家校验)
CompoundTag researchTag = new CompoundTag();
researchTag.putString(NBTKEY_RESEARCH, RESEARCH_DISTILESSENTIA);
encoded.set(DataComponents.CUSTOM_DATA, CustomData.of(researchTag));
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, encoded); encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, encoded);
} }
@@ -23,6 +23,7 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.IArcaneRecipe; import thaumcraft.api.crafting.IArcaneRecipe;
import thaumcraft.api.crafting.ShapedArcaneRecipe; import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe; import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.common.container.slot.GhostSlot; import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern; import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore; import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
@@ -220,6 +221,13 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
return CoreSaveState.Disabled_InvalidRecipe; return CoreSaveState.Disabled_InvalidRecipe;
} }
// 研究限制:配方有研究要求且玩家未解锁 → 禁止保存
String research = activeRecipe.getResearch();
if (research != null && !research.isEmpty()
&& !TCResearchManager.has(playerInv.player, research)) {
return CoreSaveState.Disabled_InvalidRecipe;
}
boolean isNew = !kCoreHandler.hasPatternFor(result); boolean isNew = !kCoreHandler.hasPatternFor(result);
if (isNew) { if (isNew) {
return kCoreHandler.hasRoomToStorePattern() return kCoreHandler.hasRoomToStorePattern()
@@ -364,6 +372,14 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
private void savePattern() { private void savePattern() {
if (activeRecipe == null) return; if (activeRecipe == null) return;
// 研究限制:配方有研究要求时,操作者必须已解锁对应研究才能编码
String research = activeRecipe.getResearch();
if (research != null && !research.isEmpty()
&& !TCResearchManager.has(playerInv.player, research)) {
ThaumicEnergistics.LOG.info("[KnowledgeInscriber] 玩家未解锁研究 {},拒绝保存奥术样板", research);
return;
}
ItemStack[] inputs = new ItemStack[CRAFTING_GRID_SIZE]; ItemStack[] inputs = new ItemStack[CRAFTING_GRID_SIZE];
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) { for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
ItemStack stack = craftingInventory.getStackInSlot(i); ItemStack stack = craftingInventory.getStackInSlot(i);
@@ -376,7 +392,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
} }
ItemStack result = resultInventory.getStackInSlot(0).copy(); ItemStack result = resultInventory.getStackInSlot(0).copy();
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs); ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs, research);
kCoreHandler.addPattern(pattern); kCoreHandler.addPattern(pattern);
updatePatternSlots(); updatePatternSlots();
sendSaveState(); sendSaveState();
@@ -72,10 +72,6 @@ public class GuiEssentiaLevelEmitter extends UpgradeableScreen<ContainerEssentia
guiGraphics.drawString(this.font, aspect.getName(), guiGraphics.drawString(this.font, aspect.getName(),
ASPECT_SLOT_X + 20, ASPECT_SLOT_Y + 4, 0xFFFFFF, false); ASPECT_SLOT_X + 20, ASPECT_SLOT_Y + 4, 0xFFFFFF, false);
} }
guiGraphics.drawString(this.font,
"Current: " + this.menu.currentLevel,
8, 70, 0xAAAAAA, false);
} }
@Override @Override
@@ -17,6 +17,7 @@ public class ArcaneCraftingPattern {
private static final String NBTKEY_INGREDIENT_NUM = "input#"; private static final String NBTKEY_INGREDIENT_NUM = "input#";
private static final String NBTKEY_RESULT = "output"; private static final String NBTKEY_RESULT = "output";
private static final String NBTKEY_ASPECTS = "aspects"; private static final String NBTKEY_ASPECTS = "aspects";
private static final String NBTKEY_RESEARCH = "research";
private static final int GRID_SIZE = 9; private static final int GRID_SIZE = 9;
/** 全局 RegistryAccess,用于 NBT 读写 */ /** 全局 RegistryAccess,用于 NBT 读写 */
@@ -27,10 +28,17 @@ public class ArcaneCraftingPattern {
protected ItemStack[] ingredients = new ItemStack[GRID_SIZE]; protected ItemStack[] ingredients = new ItemStack[GRID_SIZE];
protected ItemStack result = ItemStack.EMPTY; protected ItemStack result = ItemStack.EMPTY;
protected boolean isValid; protected boolean isValid;
/** 对应 TC 奥术配方的研究键(用于使用端按玩家校验解锁)。可为空(老数据/无研究配方)。 */
protected String research;
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients) { public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients) {
this(aspects, result, ingredients, null);
}
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients, String research) {
this.aspects = aspects.copy(); this.aspects = aspects.copy();
this.result = result.copy(); this.result = result.copy();
this.research = research;
boolean hasValidInput = false; boolean hasValidInput = false;
for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) { for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) {
@@ -72,6 +80,12 @@ public class ArcaneCraftingPattern {
return aspects.getAmount(aspect); return aspects.getAmount(aspect);
} }
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@org.jetbrains.annotations.Nullable
public String getResearch() {
return research;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static boolean canSubstituteFor(Object target, ItemStack input) { public static boolean canSubstituteFor(Object target, ItemStack input) {
if (target instanceof ItemStack targetStack) { if (target instanceof ItemStack targetStack) {
@@ -91,6 +105,10 @@ public class ArcaneCraftingPattern {
this.ingredients = new ItemStack[GRID_SIZE]; this.ingredients = new ItemStack[GRID_SIZE];
this.isValid = true; this.isValid = true;
if (data.contains(NBTKEY_RESEARCH)) {
this.research = data.getString(NBTKEY_RESEARCH);
}
if (data.contains(NBTKEY_ASPECTS)) { if (data.contains(NBTKEY_ASPECTS)) {
this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS)); this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS));
} }
@@ -133,6 +151,10 @@ public class ArcaneCraftingPattern {
this.aspects.writeToNBT(aspectTag); this.aspects.writeToNBT(aspectTag);
data.put(NBTKEY_ASPECTS, aspectTag); data.put(NBTKEY_ASPECTS, aspectTag);
if (this.research != null && !this.research.isEmpty()) {
data.putString(NBTKEY_RESEARCH, this.research);
}
for (int i = 0; i < GRID_SIZE; i++) { for (int i = 0; i < GRID_SIZE; i++) {
if (this.ingredients[i] == null || this.ingredients[i].isEmpty()) continue; if (this.ingredients[i] == null || this.ingredients[i].isEmpty()) continue;
// 1.21.1: ItemStack.save(Provider) 返回新的 Tag,不会写入传入的 CompoundTag。 // 1.21.1: ItemStack.save(Provider) 返回新的 Tag,不会写入传入的 CompoundTag。
@@ -0,0 +1,49 @@
package thaumicenergistics.common.integration.tc;
import appeng.api.crafting.IPatternDetails;
import appeng.api.stacks.AEItemKey;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* 从 AE2 {@link IPatternDetails} 中提取 TC 研究键的辅助类。
*
* 两种样板携带研究键的方式:
* - 奥术装配器样板 {@link TileArcaneAssembler.ArcaneAssemblerPattern}:直接持 research 字段
* - 蒸馏处理样板(AE2 标准 processing pattern):通过样板上 DataComponents.CUSTOM_DATA 的 research 字段标记
*/
public final class PatternResearchHelper {
public static final String NBTKEY_RESEARCH = "research";
private PatternResearchHelper() {}
/**
* 提取样板对应的 TC 研究键;无研究要求或未知样板时返回 null(表示不限制)。
*/
@Nullable
public static String getResearch(IPatternDetails pattern) {
if (pattern instanceof TileArcaneAssembler.ArcaneAssemblerPattern arcanePattern) {
return arcanePattern.getResearch();
}
// 蒸馏处理样板:从样板上 CUSTOM_DATA 读 research 标记
AEItemKey definition = pattern.getDefinition();
if (definition != null) {
ItemStack patternStack = definition.toStack();
CustomData customData = patternStack.get(DataComponents.CUSTOM_DATA);
if (customData != null) {
CompoundTag tag = customData.copyTag();
if (tag.contains(NBTKEY_RESEARCH)) {
String research = tag.getString(NBTKEY_RESEARCH);
return research.isEmpty() ? null : research;
}
}
}
return null;
}
}
@@ -0,0 +1,41 @@
package thaumicenergistics.common.network;
import net.minecraft.client.Minecraft;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.client.ResearchGateOverlay;
/**
* 服务端→客户端 网络包:告知玩家"研究未解锁"拦截发生,并携带具体研究键。
*
* 服务端在 submitJob 拦截(MixinCraftingService)时发送到对应玩家;
* 客户端收到后由 {@link ResearchGateOverlay} 在最上层渲染提示。
*/
public record ResearchGateErrorPacket(String research) implements CustomPacketPayload {
public static final Type<ResearchGateErrorPacket> TYPE =
new Type<>(ThaumicEnergistics.id("research_gate_error"));
public static final StreamCodec<RegistryFriendlyByteBuf, ResearchGateErrorPacket> STREAM_CODEC = StreamCodec.of(
(buf, pkt) -> buf.writeUtf(pkt.research),
buf -> new ResearchGateErrorPacket(buf.readUtf())
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
/**
* 客户端处理:记录最近一次研究未解锁拦截的研究键,供渲染层读取。
*/
public void handleOnClient(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
// 无论当前在什么界面,记录研究键
ResearchGateOverlay.setPendingResearch(this.research);
});
}
}
@@ -49,5 +49,12 @@ public final class ThENetworking {
CellWorkbenchSyncPacket.STREAM_CODEC, CellWorkbenchSyncPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx) (pkt, ctx) -> pkt.handleOnClient(ctx)
); );
// ResearchGateErrorPacket — S2C(服务端→客户端,研究未解锁拦截提示,携带研究键)
registrar.playToClient(
ResearchGateErrorPacket.TYPE,
ResearchGateErrorPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
} }
} }
@@ -164,13 +164,14 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
/** /**
* 找到面对中继的隧道端:优先 P2P 输入端,其次任一输出端,最后自身。 * 找到面对中继的隧道端:优先 P2P 输入端,其次任一输出端,最后自身。
* 注意:内部使用 super.getOutputs() 而非 getOutputs(),避免覆写导致递归。
*/ */
private VisInterfacePart findRelayEnd() { private VisInterfacePart findRelayEnd() {
VisInterfacePart input = getInput(); VisInterfacePart input = getInput();
if (input != null && input.getRelay() != null) { if (input != null && input.getRelay() != null) {
return input; return input;
} }
for (VisInterfacePart output : getOutputs()) { for (VisInterfacePart output : super.getOutputs()) {
if (output.getRelay() != null) { if (output.getRelay() != null) {
return output; return output;
} }
@@ -181,6 +182,25 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
return null; return null;
} }
/**
* 覆写输出端列表:半 P2P 场景下(装配器直接按频率查输入,无标准 P2P 输出端),
* 若存在有效的中继端(findRelayEnd 非空,含自身面对中继的情况),
* 将其视为已链接,使 AE2 P2P 状态显示(Jade/TOP)不再显示"Unlinked"。
*/
@Override
public List<VisInterfacePart> getOutputs() {
List<VisInterfacePart> outputs = super.getOutputs();
if (!outputs.isEmpty()) {
return outputs;
}
VisInterfacePart relayEnd = findRelayEnd();
if (relayEnd != null) {
// 含自身面对中继的情况:返回 relayEnd(可能是 this),让 P2P 状态显示已链接
return relayEnd == this ? List.of(this) : List.of(relayEnd);
}
return outputs;
}
private int drawFromRelay(VisRelayBlockEntity relay, ResourceLocation aspectId, int centivis) { private int drawFromRelay(VisRelayBlockEntity relay, ResourceLocation aspectId, int centivis) {
if (relay == null) { if (relay == null) {
return 0; return 0;
@@ -483,7 +483,8 @@ public class TileArcaneAssembler extends ThETileBase
} }
} }
return new ArcaneAssemblerPattern(AEItemKey.of(result), null, aspects, result, inputs); return new ArcaneAssemblerPattern(AEItemKey.of(result), null, aspects, result, inputs,
acp.getResearch());
} }
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) { private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
@@ -751,14 +752,22 @@ public class TileArcaneAssembler extends ThETileBase
private final AspectList aspects; private final AspectList aspects;
private final ItemStack resultStack; private final ItemStack resultStack;
private final IInput[] inputs; private final IInput[] inputs;
private final String research;
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe, public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack, AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks) { List<GenericStack> inputStacks) {
this(definition, recipe, aspects, resultStack, inputStacks, null);
}
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks, String research) {
this.definition = definition; this.definition = definition;
this.recipe = recipe; this.recipe = recipe;
this.aspects = aspects; this.aspects = aspects;
this.resultStack = resultStack; this.resultStack = resultStack;
this.research = research;
this.inputs = new IInput[inputStacks.size()]; this.inputs = new IInput[inputStacks.size()];
for (int i = 0; i < inputStacks.size(); i++) { for (int i = 0; i < inputStacks.size(); i++) {
GenericStack stack = inputStacks.get(i); GenericStack stack = inputStacks.get(i);
@@ -820,5 +829,8 @@ public class TileArcaneAssembler extends ThETileBase
public IArcaneRecipe getRecipe() { return recipe; } public IArcaneRecipe getRecipe() { return recipe; }
public AspectList getAspects() { return aspects; } public AspectList getAspects() { return aspects; }
public ItemStack getResultStack() { return resultStack; } public ItemStack getResultStack() { return resultStack; }
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@Nullable
public String getResearch() { return research; }
} }
} }
@@ -0,0 +1,29 @@
package thaumicenergistics.mixins.ae2;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import thaumicenergistics.client.ResearchGateOverlay;
/**
* 拦截 CraftConfirmScreen 打开 CPU 忙错误屏。
*
* 研究未解锁拦截会触发 AE2 的 CPU_BUSY 错误屏;本 Mixin 在错误屏被 switchToScreen 之前
* (updateBeforeRender HEAD)检测到研究拦截(ResearchGateOverlay 已有网络包写入的研究键),
* 取消原渲染逻辑,避免打开误导性的"CPU 忙"窗口。
* 提示文字由 ResearchGateOverlay 在无 GUI 遮挡时绘制。
*/
@Mixin(targets = "appeng.client.gui.me.crafting.CraftConfirmScreen", remap = false)
public abstract class MixinCraftConfirmScreen {
@Inject(method = "updateBeforeRender()V", at = @At("HEAD"), cancellable = true, remap = false)
private void thaumicenergistics$blockCpuBusyScreen(CallbackInfo ci) {
// 网络包已写入研究键 = 本次失败是研究拦截,而非真实 CPU 忙碌
// 使用 submitError 状态而非时间戳:只要错误未被玩家清除就持续拦截,避免过期后弹窗
if (ResearchGateOverlay.hasResearchGateError()) {
ci.cancel();
}
}
}
@@ -0,0 +1,73 @@
package thaumicenergistics.mixins.ae2;
import java.util.Map;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.crafting.CraftingSubmitErrorCode;
import appeng.api.networking.crafting.ICraftingCPU;
import appeng.api.networking.crafting.ICraftingPlan;
import appeng.api.networking.crafting.ICraftingRequester;
import appeng.api.networking.crafting.ICraftingSubmitResult;
import appeng.api.networking.security.IActionSource;
import appeng.crafting.execution.CraftingSubmitResult;
import net.minecraft.world.entity.player.Player;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.PatternResearchHelper;
/**
* 使用端研究限制:玩家从 AE 终端发起合成请求时(submitJob),
* 若请求涉及尚未解锁对应 TC 研究的样板(奥术/蒸馏),则拒绝该请求。
*
* 机器自动合成(IActionSource 无玩家)不拦截。
*/
@Mixin(targets = "appeng.me.service.CraftingService", remap = false)
public abstract class MixinCraftingService {
@Inject(method = "submitJob", at = @At("HEAD"), cancellable = true, remap = false)
private void thaumicenergistics$checkResearchBeforeSubmit(
ICraftingPlan job,
ICraftingRequester requestingMachine,
ICraftingCPU target,
boolean prioritizePower,
IActionSource src,
CallbackInfoReturnable<ICraftingSubmitResult> cir) {
// 仅玩家发起的请求需要研究校验;机器自动合成放行
if (src == null || src.player().isEmpty()) {
return;
}
Player player = src.player().get();
if (player == null) {
return;
}
for (Map.Entry<IPatternDetails, Long> entry : job.patternTimes().entrySet()) {
IPatternDetails pattern = entry.getKey();
String research = PatternResearchHelper.getResearch(pattern);
if (research == null || research.isEmpty()) {
continue; // 无研究要求
}
if (!TCResearchManager.has(player, research)) {
ThaumicEnergistics.LOG.info(
"[AE2ResearchGate] 玩家 {} 未解锁研究 {},拒绝合成请求 (pattern={})",
player.getName().getString(), research, pattern.getDefinition());
// 通过自定义网络包发送研究键到客户端(AE2 errorDetail 不跨网络传输)
if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
net.neoforged.neoforge.network.PacketDistributor.sendToPlayer(
serverPlayer,
new thaumicenergistics.common.network.ResearchGateErrorPacket(research));
}
// 拒绝提交(CPU_BUSY 触发 AE2 错误屏;具体提示由客户端网络包渲染)
cir.setReturnValue(CraftingSubmitResult.simpleError(CraftingSubmitErrorCode.CPU_BUSY));
return;
}
}
}
}
@@ -37,6 +37,9 @@
"tooltip.thaumicenergistics.essentia_storage_bus.desc": "Bidirectionally syncs an external essentia container with the ME network", "tooltip.thaumicenergistics.essentia_storage_bus.desc": "Bidirectionally syncs an external essentia container with the ME network",
"tooltip.thaumicenergistics.essentia_storage_bus.hint": "Place on a cable facing an essentia container to use it as network storage", "tooltip.thaumicenergistics.essentia_storage_bus.hint": "Place on a cable facing an essentia container to use it as network storage",
"tooltip.thaumicenergistics.essentia_terminal.desc": "View and extract essentia from the ME network", "tooltip.thaumicenergistics.essentia_terminal.desc": "View and extract essentia from the ME network",
"tooltip.thaumicenergistics.arcane_crafting_terminal.desc": "Perform arcane crafting from the ME network",
"tooltip.thaumicenergistics.essentia_level_emitter.desc": "Emits redstone based on essentia levels in the network",
"tooltip.thaumicenergistics.vis_interface.desc": "Bridges the Thaumcraft vis network with the ME network for vis extraction/supply",
"ae2.keytype.thaumicenergistics.essentia": "Essentia", "ae2.keytype.thaumicenergistics.essentia": "Essentia",
"tc.research_category.THAUMICENERGISTICS": "Thaumic Energistics", "tc.research_category.THAUMICENERGISTICS": "Thaumic Energistics",
"tc.research_name.BASEENERGISTICS": "Basic Energistics", "tc.research_name.BASEENERGISTICS": "Basic Energistics",
@@ -38,6 +38,9 @@
"tooltip.thaumicenergistics.essentia_storage_bus.desc": "将相邻的源质容器与ME网络双向同步", "tooltip.thaumicenergistics.essentia_storage_bus.desc": "将相邻的源质容器与ME网络双向同步",
"tooltip.thaumicenergistics.essentia_storage_bus.hint": "贴在线缆上,把外部源质容器当作ME存储单元使用", "tooltip.thaumicenergistics.essentia_storage_bus.hint": "贴在线缆上,把外部源质容器当作ME存储单元使用",
"tooltip.thaumicenergistics.essentia_terminal.desc": "查看并取出/存入源质", "tooltip.thaumicenergistics.essentia_terminal.desc": "查看并取出/存入源质",
"tooltip.thaumicenergistics.arcane_crafting_terminal.desc": "在ME网络上进行奥术合成",
"tooltip.thaumicenergistics.essentia_level_emitter.desc": "根据网络中源质数量输出红石信号",
"tooltip.thaumicenergistics.vis_interface.desc": "将TC要素网络与ME网络桥接,可抽取/供给要素",
"ae2.keytype.thaumicenergistics.essentia": "源质", "ae2.keytype.thaumicenergistics.essentia": "源质",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"refmap": "thaumicenergistics.refmap.json", "refmap": "thaumicenergistics.refmap.json",
"mixins": [ "mixins": [
"ae2.MixinCraftingService",
"ae2.MixinCraftingTreeProcess", "ae2.MixinCraftingTreeProcess",
"ae2.MixinAbstractTerminalPart", "ae2.MixinAbstractTerminalPart",
"ae2.MixinConfigMenuInventory", "ae2.MixinConfigMenuInventory",
@@ -11,6 +12,7 @@
"tc.MixinVisRelayBlock" "tc.MixinVisRelayBlock"
], ],
"client": [ "client": [
"ae2.MixinCraftConfirmScreen",
"tc.ResearchPageIndexMixin", "tc.ResearchPageIndexMixin",
"tc.MixinThaumonomiconScreen" "tc.MixinThaumonomiconScreen"
], ],