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

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,8 +4,10 @@ import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import appeng.core.definitions.AEItems;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.neoforged.neoforge.network.PacketDistributor;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
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.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.ThaumcraftApiHelper;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.research.TCResearchManager;
import thaumcraft.common.research.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
@@ -31,6 +35,10 @@ import java.util.List;
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 HOTBAR_Y = 208;
private static final int PLAYER_INV_X = 8;
@@ -249,6 +257,13 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
public void onEncodePattern() {
if (encoder == null) return;
// 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板
if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) {
ThaumicEnergistics.LOG.info("[DistillationEncoder] 玩家未解锁研究 {},拒绝编码蒸馏样板", RESEARCH_DISTILESSENTIA);
return;
}
Aspect selected = getSelectedAspect();
ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
@@ -288,6 +303,11 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
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);
}
@@ -23,6 +23,7 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.IArcaneRecipe;
import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
@@ -220,6 +221,13 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
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);
if (isNew) {
return kCoreHandler.hasRoomToStorePattern()
@@ -364,6 +372,14 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
private void savePattern() {
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];
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
ItemStack stack = craftingInventory.getStackInSlot(i);
@@ -376,7 +392,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
}
ItemStack result = resultInventory.getStackInSlot(0).copy();
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs);
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs, research);
kCoreHandler.addPattern(pattern);
updatePatternSlots();
sendSaveState();
@@ -72,10 +72,6 @@ public class GuiEssentiaLevelEmitter extends UpgradeableScreen<ContainerEssentia
guiGraphics.drawString(this.font, aspect.getName(),
ASPECT_SLOT_X + 20, ASPECT_SLOT_Y + 4, 0xFFFFFF, false);
}
guiGraphics.drawString(this.font,
"Current: " + this.menu.currentLevel,
8, 70, 0xAAAAAA, false);
}
@Override
@@ -17,6 +17,7 @@ public class ArcaneCraftingPattern {
private static final String NBTKEY_INGREDIENT_NUM = "input#";
private static final String NBTKEY_RESULT = "output";
private static final String NBTKEY_ASPECTS = "aspects";
private static final String NBTKEY_RESEARCH = "research";
private static final int GRID_SIZE = 9;
/** 全局 RegistryAccess,用于 NBT 读写 */
@@ -27,10 +28,17 @@ public class ArcaneCraftingPattern {
protected ItemStack[] ingredients = new ItemStack[GRID_SIZE];
protected ItemStack result = ItemStack.EMPTY;
protected boolean isValid;
/** 对应 TC 奥术配方的研究键(用于使用端按玩家校验解锁)。可为空(老数据/无研究配方)。 */
protected String research;
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.result = result.copy();
this.research = research;
boolean hasValidInput = false;
for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) {
@@ -72,6 +80,12 @@ public class ArcaneCraftingPattern {
return aspects.getAmount(aspect);
}
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@org.jetbrains.annotations.Nullable
public String getResearch() {
return research;
}
@SuppressWarnings("unchecked")
public static boolean canSubstituteFor(Object target, ItemStack input) {
if (target instanceof ItemStack targetStack) {
@@ -91,6 +105,10 @@ public class ArcaneCraftingPattern {
this.ingredients = new ItemStack[GRID_SIZE];
this.isValid = true;
if (data.contains(NBTKEY_RESEARCH)) {
this.research = data.getString(NBTKEY_RESEARCH);
}
if (data.contains(NBTKEY_ASPECTS)) {
this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS));
}
@@ -133,6 +151,10 @@ public class ArcaneCraftingPattern {
this.aspects.writeToNBT(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++) {
if (this.ingredients[i] == null || this.ingredients[i].isEmpty()) continue;
// 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,
(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 输入端,其次任一输出端,最后自身。
* 注意:内部使用 super.getOutputs() 而非 getOutputs(),避免覆写导致递归。
*/
private VisInterfacePart findRelayEnd() {
VisInterfacePart input = getInput();
if (input != null && input.getRelay() != null) {
return input;
}
for (VisInterfacePart output : getOutputs()) {
for (VisInterfacePart output : super.getOutputs()) {
if (output.getRelay() != null) {
return output;
}
@@ -181,6 +182,25 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
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) {
if (relay == null) {
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) {
@@ -751,14 +752,22 @@ public class TileArcaneAssembler extends ThETileBase
private final AspectList aspects;
private final ItemStack resultStack;
private final IInput[] inputs;
private final String research;
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
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.recipe = recipe;
this.aspects = aspects;
this.resultStack = resultStack;
this.research = research;
this.inputs = new IInput[inputStacks.size()];
for (int i = 0; i < inputStacks.size(); i++) {
GenericStack stack = inputStacks.get(i);
@@ -820,5 +829,8 @@ public class TileArcaneAssembler extends ThETileBase
public IArcaneRecipe getRecipe() { return recipe; }
public AspectList getAspects() { return aspects; }
public ItemStack getResultStack() { return resultStack; }
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@Nullable
public String getResearch() { return research; }
}
}