This commit is contained in:
1820982382
2026-08-04 16:34:40 +08:00
parent bb6e4b9446
commit 83143de9d3
27 changed files with 568 additions and 104 deletions
@@ -5,12 +5,15 @@ import net.minecraft.network.RegistryFriendlyByteBuf;
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;
import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
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.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.IVisDiscountGear; import thaumcraft.api.IVisDiscountGear;
import thaumcraft.api.IWarpingGear; import thaumcraft.api.IWarpingGear;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileArcaneAssembler; import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
@@ -81,6 +84,47 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
public final TileArcaneAssembler assembler; public final TileArcaneAssembler assembler;
/** 可视数据同步:服务端读 tile,客户端存同步值(GUI 进度条/源质条不依赖 BE 网络同步,靠容器每 tick 同步) */
public final ContainerData visData;
/** 索引约定 */
public static final int DATA_AIR = 0, DATA_WATER = 1, DATA_FIRE = 2, DATA_ORDER = 3,
DATA_ENTROPY = 4, DATA_EARTH = 5, DATA_CRAFTING = 6, DATA_CRAFT_TICK = 7, DATA_TICKS_PER_CRAFT = 8;
public static final int DATA_COUNT = 9;
/** 服务端:每次 get 都从 tile 读实时值(broadcastChanges 每 tick 调用) */
private static class TileVisData implements ContainerData {
private final TileArcaneAssembler tile;
TileVisData(TileArcaneAssembler tile) { this.tile = tile; }
/** 节流:每 4 单位才变一次(broadcastChanges 只发变化的值 → 发包频率降 4 倍,进度条仍流畅) */
private static int throttle(int v) { return (v / 4) * 4; }
@Override public int get(int index) {
if (tile == null || tile.getStoredVis() == null) return 0;
return switch (index) {
case DATA_AIR -> throttle(tile.getStoredVis().getAmount(Aspect.AIR));
case DATA_WATER -> throttle(tile.getStoredVis().getAmount(Aspect.WATER));
case DATA_FIRE -> throttle(tile.getStoredVis().getAmount(Aspect.FIRE));
case DATA_ORDER -> throttle(tile.getStoredVis().getAmount(Aspect.ORDER));
case DATA_ENTROPY -> throttle(tile.getStoredVis().getAmount(Aspect.ENTROPY));
case DATA_EARTH -> throttle(tile.getStoredVis().getAmount(Aspect.EARTH));
case DATA_CRAFTING -> tile.isCrafting() ? 1 : 0;
case DATA_CRAFT_TICK -> throttle(tile.getCraftTickCounter());
case DATA_TICKS_PER_CRAFT -> tile.getTicksPerCraft();
default -> 0;
};
}
@Override public void set(int index, int value) { /* 服务端只读 */ }
@Override public int getCount() { return DATA_COUNT; }
}
/** 客户端:保存服务端同步来的值 */
private static class ClientVisData implements ContainerData {
private final int[] values = new int[DATA_COUNT];
@Override public int get(int index) { return index >= 0 && index < DATA_COUNT ? values[index] : 0; }
@Override public void set(int index, int value) { if (index >= 0 && index < DATA_COUNT) values[index] = value; }
@Override public int getCount() { return DATA_COUNT; }
}
/** 服务端构造(由 Block#getMenuProvider 调用,不带 tile */ /** 服务端构造(由 Block#getMenuProvider 调用,不带 tile */
public ContainerArcaneAssembler(int id, Inventory inv) { public ContainerArcaneAssembler(int id, Inventory inv) {
this(id, inv, (TileArcaneAssembler) null); this(id, inv, (TileArcaneAssembler) null);
@@ -90,6 +134,13 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
public ContainerArcaneAssembler(int id, Inventory inv, TileArcaneAssembler tile) { public ContainerArcaneAssembler(int id, Inventory inv, TileArcaneAssembler tile) {
super(ModMenuTypes.ARCANE_ASSEMBLER.get(), id); super(ModMenuTypes.ARCANE_ASSEMBLER.get(), id);
this.assembler = tile; this.assembler = tile;
// 可视数据同步:服务端从 tile 读实时值;客户端存同步值(不依赖 BE 网络同步)
if (inv.player.level().isClientSide) {
this.visData = new ClientVisData();
} else {
this.visData = new TileVisData(tile);
}
this.addDataSlots(this.visData);
// ===== 1. 玩家背包 3行 ===== // ===== 1. 玩家背包 3行 =====
for (int r = 0; r < 3; r++) for (int r = 0; r < 3; r++)
@@ -166,7 +217,16 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
/** 客户端构造(IMenuTypeExtension 反射调用) */ /** 客户端构造(IMenuTypeExtension 反射调用) */
public ContainerArcaneAssembler(int id, Inventory inv, RegistryFriendlyByteBuf buf) { public ContainerArcaneAssembler(int id, Inventory inv, RegistryFriendlyByteBuf buf) {
this(id, inv); this(id, inv, resolveTile(inv, buf));
}
/** 临时诊断:确认客户端能否拿到 tile(定位后删除) */
private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) {
var pos = buf.readBlockPos();
var be = inv.player.level().getBlockEntity(pos);
ThaumicEnergistics.LOG.info("[AA-DEBUG] client ctor: pos={}, be={}",
pos, be == null ? "null" : be.getClass().getSimpleName());
return (TileArcaneAssembler) be;
} }
private ItemStackHandler wrapMachineInventory() { private ItemStackHandler wrapMachineInventory() {
@@ -88,6 +88,13 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return 1; return 1;
} }
@Override
public boolean mayPlace(ItemStack stack) {
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
return stack.getItem() instanceof TCFunctionalItems.WandCastingItem;
}
@Override @Override
public void setChanged() { public void setChanged() {
super.setChanged(); super.setChanged();
@@ -5,6 +5,7 @@ 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.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.neoforged.neoforge.network.PacketDistributor;
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;
@@ -21,6 +22,7 @@ import thaumcraft.common.research.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey; import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection; import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
import thaumicenergistics.common.tiles.TileDistillationPatternEncoder; import thaumicenergistics.common.tiles.TileDistillationPatternEncoder;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
import java.util.Arrays; import java.util.Arrays;
@@ -104,14 +106,18 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
}); });
if (encoder != null) { if (encoder != null) {
// 源物品槽:绑定 Tile 槽 0(持久化到方块 NBT,关闭 GUI 不丢失 // 源物品槽:ghost 模板(背包拖入不消耗——实际物品还在手里;空手点击清空;JEI 拖拽设置模板
addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) { addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) {
@Override @Override
public int getMaxStackSize() { return 1; } public int getMaxStackSize() { return 1; }
@Override
public boolean mayPlace(ItemStack s) { return true; }
@Override
public boolean mayPickup(Player p) { return false; }
}); });
addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) { addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) {
@Override @Override
public boolean mayPlace(ItemStack s) { return true; } public boolean mayPlace(ItemStack s) { return AEItems.BLANK_PATTERN.is(s); }
@Override @Override
public int getMaxStackSize() { return 64; } public int getMaxStackSize() { return 64; }
}); });
@@ -125,10 +131,14 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) { addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) {
@Override @Override
public int getMaxStackSize() { return 1; } public int getMaxStackSize() { return 1; }
@Override
public boolean mayPlace(ItemStack s) { return true; }
@Override
public boolean mayPickup(Player p) { return false; }
}); });
addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) { addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) {
@Override @Override
public boolean mayPlace(ItemStack s) { return true; } public boolean mayPlace(ItemStack s) { return AEItems.BLANK_PATTERN.is(s); }
@Override @Override
public int getMaxStackSize() { return 64; } public int getMaxStackSize() { return 64; }
}); });
@@ -150,6 +160,14 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
return ItemStack.EMPTY; return ItemStack.EMPTY;
} }
/** 服务端:设置源物品(JEI 拖拽)——写入 encoder 槽 0 + 刷新 aspects。 */
public void setSourceItemOnServer(ItemStack stack) {
if (encoder == null) return;
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, stack.copy());
encoder.setChanged();
updateAspects();
}
/** /**
* 源物品的 aspects:源物品变化(含 NBT/扫描数据)时才调用 {@code getObjectAspects} 重新解析, * 源物品的 aspects:源物品变化(含 NBT/扫描数据)时才调用 {@code getObjectAspects} 重新解析,
* 否则返回缓存——每帧渲染调用仅做 O(1) 物品比较,不频繁请求 TC。 * 否则返回缓存——每帧渲染调用仅做 O(1) 物品比较,不频繁请求 TC。
@@ -235,13 +253,6 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
ItemStack source = getSourceItem(); ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS); ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
ItemStack encodedSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN); ItemStack encodedSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN);
ThaumicEnergistics.LOG.info("[DE] onEncodePattern: selected={}, selIdx={}, aspects={}, source={}, blank={}isBlank={}, encoded={}",
selected == null ? "null" : selected.getTag(), selectedAspectIndex,
getSourceAspects().length,
source.isEmpty() ? "EMPTY" : source.getItem().getDescriptionId(),
blankSlot.isEmpty() ? "EMPTY" : blankSlot.getItem().getDescriptionId(),
AEItems.BLANK_PATTERN.is(blankSlot),
encodedSlot.isEmpty() ? "EMPTY" : encodedSlot.getItem().getDescriptionId());
if (selected == null) return; if (selected == null) return;
@@ -251,8 +262,6 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
// 验证必须是 AE2 空白样板,否则不消耗不编码(日志定位 is() 运行时行为) // 验证必须是 AE2 空白样板,否则不消耗不编码(日志定位 is() 运行时行为)
if (!AEItems.BLANK_PATTERN.is(blankSlot)) { if (!AEItems.BLANK_PATTERN.is(blankSlot)) {
ThaumicEnergistics.LOG.warn("[DE] Encode skipped: blank slot item={}, isBlankPattern={}",
blankSlot.getItem().getDescriptionId(), AEItems.BLANK_PATTERN.is(blankSlot));
return; return;
} }
@@ -288,11 +297,18 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
Slot slot = slots.get(slotId); Slot slot = slots.get(slotId);
if (slotId == SOURCE_SLOT) { if (slotId == SOURCE_SLOT) {
// 标准拖放处理(SlotItemHandler 已限制槽内 1 个),刷新 aspects // ghost 模板语义:手持物品→设模板(不消耗,实际物品留在手里);空手→清空
super.clicked(slotId, dragType, clickType, player); ItemStack carried = getCarried();
ItemStack newSource = carried.isEmpty() ? ItemStack.EMPTY : carried.copyWithCount(1);
if (player.level().isClientSide) {
if (slotId < slots.size()) {
slots.get(slotId).set(newSource);
}
PacketDistributor.sendToServer(new DistillationEncoderSetSourceC2SPacket(containerId, newSource));
} else {
setSourceItemOnServer(newSource);
}
updateAspects(); updateAspects();
ThaumicEnergistics.LOG.info("[DE] source slot clicked: item={}, count={}, aspects={}",
getSourceItem().getItem().getDescriptionId(), getSourceItem().getCount(), getSourceAspects().length);
return; return;
} }
@@ -300,8 +316,6 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
int aspectIdx = slotId - ASPECT_SLOT_START; int aspectIdx = slotId - ASPECT_SLOT_START;
// 始终记录用户选择(越界由 getSelectedAspect 校验),避免因 cachedAspects 状态导致服务端选择丢失 // 始终记录用户选择(越界由 getSelectedAspect 校验),避免因 cachedAspects 状态导致服务端选择丢失
selectedAspectIndex = aspectIdx; selectedAspectIndex = aspectIdx;
ThaumicEnergistics.LOG.info("[DE] aspect clicked: idx={}, cached={}, selected={}",
aspectIdx, cachedAspects.length, selectedAspectIndex);
// selected 槽留空,图标由 GUI 绘制 // selected 槽留空,图标由 GUI 绘制
aspectInventory.setStackInSlot(SELECTED_SLOT - ASPECT_SLOT_START, ItemStack.EMPTY); aspectInventory.setStackInSlot(SELECTED_SLOT - ASPECT_SLOT_START, ItemStack.EMPTY);
return; return;
@@ -105,8 +105,6 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
super(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), id); super(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), id);
this.tile = tile; this.tile = tile;
this.playerInv = inv; this.playerInv = inv;
ThaumicEnergistics.LOG.info("[KLGE] ContainerKnowledgeInscriber opened, id={}, hasTile={}, isClient={}",
id, tile != null, inv.player.level().isClientSide());
for (int r = 0; r < 3; r++) for (int r = 0; r < 3; r++)
for (int c = 0; c < 9; c++) for (int c = 0; c < 9; c++)
@@ -240,14 +238,11 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
} }
activeRecipe = findArcaneRecipe(); activeRecipe = findArcaneRecipe();
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: activeRecipe={}", activeRecipe);
if (activeRecipe != null) { if (activeRecipe != null) {
ItemStack output = activeRecipe.getCraftingResult(craftingContainer); ItemStack output = activeRecipe.getCraftingResult(craftingContainer);
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: output={}", output.isEmpty() ? "EMPTY" : output.getItem().toString());
resultInventory.setStackInSlot(0, output); resultInventory.setStackInSlot(0, output);
} else { } else {
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: no recipe matched → EMPTY");
resultInventory.setStackInSlot(0, ItemStack.EMPTY); resultInventory.setStackInSlot(0, ItemStack.EMPTY);
} }
@@ -257,28 +252,21 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
private IArcaneRecipe findArcaneRecipe() { private IArcaneRecipe findArcaneRecipe() {
Level level = playerInv.player.level(); Level level = playerInv.player.level();
if (level == null) { if (level == null) {
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: level is null");
return null; return null;
} }
var recipes = ThaumcraftApi.getCraftingRecipes(); var recipes = ThaumcraftApi.getCraftingRecipes();
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: total recipes={}", recipes.size());
int arcaneCount = 0; int arcaneCount = 0;
for (Object recipe : recipes) { for (Object recipe : recipes) {
if (recipe instanceof IArcaneRecipe arcaneRecipe) { if (recipe instanceof IArcaneRecipe arcaneRecipe) {
arcaneCount++; arcaneCount++;
try { try {
if (gridMatches(arcaneRecipe, craftingContainer)) { if (gridMatches(arcaneRecipe, craftingContainer)) {
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: MATCH FOUND! recipe={}",
arcaneRecipe);
return arcaneRecipe; return arcaneRecipe;
} }
} catch (Exception e) { } catch (Exception e) {
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: exception on recipe={}: {}",
arcaneRecipe, e.getMessage());
} }
} }
} }
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: no match among {} arcane recipes", arcaneCount);
return null; return null;
} }
@@ -359,7 +347,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
private void sendSaveState() { private void sendSaveState() {
if (tile != null && tile.getLevel() != null && !tile.getLevel().isClientSide) { if (tile != null && tile.getLevel() != null && !tile.getLevel().isClientSide) {
saveStateSlot.set(getSaveState().ordinal()); CoreSaveState s = getSaveState();
saveStateSlot.set(s.ordinal());
} }
} }
@@ -423,25 +412,32 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
public void clicked(int slotId, int dragType, ClickType clickType, Player player) { public void clicked(int slotId, int dragType, ClickType clickType, Player player) {
if (slotId >= 0 && slotId < slots.size()) { if (slotId >= 0 && slotId < slots.size()) {
Slot slot = slots.get(slotId); Slot slot = slots.get(slotId);
// 图案槽(核心已有配方,slotId 37..37+MAXIMUM_PATTERNS-1):点击加载该配方到合成网格 → 可保存/删除
if (slotId >= 37 && slotId < 37 + MAXIMUM_PATTERNS) {
int patternIdx = slotId - 37;
ItemStack output = patternInventory.getStackInSlot(patternIdx);
if (!output.isEmpty()) {
ArcaneCraftingPattern pattern = kCoreHandler.getPatternForItem(output);
if (pattern != null) {
loadPatternToGrid(pattern, player);
}
}
return;
}
if (slot instanceof GhostSlot ghostSlot) { if (slot instanceof GhostSlot ghostSlot) {
int gridIndex = ghostSlot.index; int gridIndex = ghostSlot.index;
ItemStack carried = getCarried(); ItemStack carried = getCarried();
ItemStack newStack = carried.isEmpty() ItemStack newStack = carried.isEmpty()
? ItemStack.EMPTY ? ItemStack.EMPTY
: carried.copyWithCount(1); : carried.copyWithCount(1);
ThaumicEnergistics.LOG.info("[KLGE] clicked GhostSlot idx={} side={} stack={}",
gridIndex, player.level().isClientSide() ? "CLIENT" : "SERVER",
newStack.isEmpty() ? "EMPTY" : newStack.getItem().toString());
ghostSlot.set(newStack); ghostSlot.set(newStack);
if (player.level().isClientSide) { if (player.level().isClientSide) {
ThaumicEnergistics.LOG.info("[KLGE] CLIENT branch → onCraftingChangedClient + C2S");
onCraftingChangedClient(); onCraftingChangedClient();
PacketDistributor.sendToServer( PacketDistributor.sendToServer(
new KnowledgeInscriberGhostSlotPacket(containerId, gridIndex, newStack)); new KnowledgeInscriberGhostSlotPacket(containerId, gridIndex, newStack));
} else { } else {
ThaumicEnergistics.LOG.info("[KLGE] SERVER branch → onCraftingChanged");
onCraftingChanged(); onCraftingChanged();
} }
return; return;
@@ -453,8 +449,6 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
public void setGhostSlotOnServer(int slotIndex, ItemStack stack) { public void setGhostSlotOnServer(int slotIndex, ItemStack stack) {
if (slotIndex < 0 || slotIndex >= CRAFTING_GRID_SIZE) return; if (slotIndex < 0 || slotIndex >= CRAFTING_GRID_SIZE) return;
ItemStack s = stack == null ? ItemStack.EMPTY : stack.copy(); ItemStack s = stack == null ? ItemStack.EMPTY : stack.copy();
ThaumicEnergistics.LOG.info("[KLGE] setGhostSlotOnServer idx={} stack={}",
slotIndex, s.isEmpty() ? "EMPTY" : s.getItem().toString());
craftingContainer.setItem(slotIndex, s); craftingContainer.setItem(slotIndex, s);
int slotId = 37 + MAXIMUM_PATTERNS + slotIndex; int slotId = 37 + MAXIMUM_PATTERNS + slotIndex;
if (slotId < slots.size() && slots.get(slotId) instanceof GhostSlot) { if (slotId < slots.size() && slots.get(slotId) instanceof GhostSlot) {
@@ -463,8 +457,37 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
onCraftingChanged(); onCraftingChanged();
} }
private void onCraftingChangedClient() { /** GhostSlot 的网格索引(0-8)。ghostSlot.index 是菜单槽序号(37+MAXIMUM_PATTERNS+idx),需换算。 */
ThaumicEnergistics.LOG.info("[KLGE] onCraftingChangedClient"); public int getGhostGridIndex(Slot slot) {
if (slot instanceof GhostSlot && slot.container == craftingContainer) {
return slot.index - (37 + MAXIMUM_PATTERNS);
}
return -1;
}
/** 点击图案槽:把核心已有配方加载到合成网格(清空 + 填 9 格材料),供保存/删除。 */
private void loadPatternToGrid(ArcaneCraftingPattern pattern, Player player) {
ItemStack[] ingredients = pattern.getIngredients();
boolean client = player.level().isClientSide;
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
ItemStack s = i < ingredients.length && ingredients[i] != null ? ingredients[i].copy() : ItemStack.EMPTY;
craftingContainer.setItem(i, s.copy());
int slotId = 37 + MAXIMUM_PATTERNS + i;
if (slotId < slots.size() && slots.get(slotId) instanceof GhostSlot gs) {
gs.set(s.copy());
}
if (client) {
PacketDistributor.sendToServer(new KnowledgeInscriberGhostSlotPacket(containerId, i, s.copy()));
}
}
if (client) {
onCraftingChangedClient();
} else {
onCraftingChanged();
}
}
public void onCraftingChangedClient() {
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) { for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy()); craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy());
} }
@@ -472,7 +495,6 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
} }
private void onCraftingChanged() { private void onCraftingChanged() {
ThaumicEnergistics.LOG.info("[KLGE] onCraftingChanged");
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) { for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy()); craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy());
} }
@@ -5,9 +5,8 @@ import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
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 thaumcraft.api.aspects.Aspect; import net.minecraft.world.inventory.ContainerData;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/** /**
* 奥术装配器 GUI。 * 奥术装配器 GUI。
@@ -27,17 +26,15 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png"); ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png");
// ===== 第4块 Vis条 ===== // ===== 第4块 Vis条 =====
private static final int VIS_BAR_X = 42; /** 纹理中空槽(暗色背景)的 Y 坐标 = 进度条在 GUI 中的显示位置 */
private static final int VIS_BAR_Y = 202; private static final int VIS_EMPTY_Y = 87;
/** 纹理中满亮条的 Y 坐标(裁剪源,不直接显示) */
private static final int VIS_FULL_Y = 202;
private static final int VIS_BAR_W = 4; private static final int VIS_BAR_W = 4;
private static final int VIS_BAR_H = 16; private static final int VIS_BAR_H = 16;
private static final int VIS_BAR_STEP = 18; private static final int VIS_BAR_STEP = 18;
/** 贴图规范起始 X = 41(步长1841,59,77,95,113,131,149 */
// ===== 6原质 ===== private static final int VIS_BAR_X = 41;
private static final Aspect[] VIS_ASPECTS = {
Aspect.AIR, Aspect.FIRE, Aspect.WATER,
Aspect.EARTH, Aspect.ORDER, Aspect.ENTROPY
};
public GuiArcaneAssembler(ContainerArcaneAssembler menu, Inventory inv, Component title) { public GuiArcaneAssembler(ContainerArcaneAssembler menu, Inventory inv, Component title) {
super(menu, inv, title); super(menu, inv, title);
@@ -64,35 +61,48 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
// 第3块:右下正方形 68×68 // 第3块:右下正方形 68×68
g.blit(TEX, left + 180, top + 129, 180, 129, 68, 68); g.blit(TEX, left + 180, top + 129, 180, 129, 68, 68);
// ===== 第4块:Vis条(纹理裁剪方式,仿1.7.10 drawVisBar ===== // ===== 第4块:Vis条(1.7.10 裁剪方式:Y=87 空槽背景 + Y=202 亮条裁剪 =====
TileArcaneAssembler tile = this.menu.assembler; // 数据源:容器同步(menu.visData,服务端每 tick 同步 storedVis/isCrafting/进度——不依赖 BE 网络同步)
ContainerData data = this.menu.visData;
final int maxCvis = 187 * 10; final int maxCvis = 187 * 10;
for (int i = 0; i < 7; i++) { for (int i = 0; i < 7; i++) {
int bx = left + VIS_BAR_X + i * VIS_BAR_STEP; int bx = left + VIS_BAR_X + i * VIS_BAR_STEP;
int by = top + VIS_BAR_Y; int by = top + VIS_EMPTY_Y;
// 从纹理 Y=87 裁剪空槽背景
g.blit(TEX, bx, by, VIS_BAR_X + i * VIS_BAR_STEP, VIS_EMPTY_Y, VIS_BAR_W, VIS_BAR_H);
float ratio = 0f; float ratio = 0f;
if (tile != null) { if (i < 6) {
if (i < 6) { // 1-6格:源质充能比例
int cvis = tile.getStoredVis() != null ? tile.getStoredVis().getAmount(VIS_ASPECTS[i]) : 0; int cvis = data.get(ContainerArcaneAssembler.DATA_AIR + i);
ratio = Math.min(1f, (float) cvis / maxCvis); ratio = Math.min(1f, (float) cvis / maxCvis);
} else {
// 第7格:非合成时要素接入常亮,合成时显示渐变进度
if (data.get(ContainerArcaneAssembler.DATA_CRAFTING) == 1) {
int tick = data.get(ContainerArcaneAssembler.DATA_CRAFT_TICK);
int total = Math.max(1, data.get(ContainerArcaneAssembler.DATA_TICKS_PER_CRAFT));
ratio = Math.min(1f, (float) tick / total);
} else { } else {
ratio = tile.getCraftProgress(); // 要素接入达成:任一原质充入(源质已从网络流入)→ 常亮
boolean anyVis = false;
for (int a = 0; a < 6; a++) {
if (data.get(ContainerArcaneAssembler.DATA_AIR + a) > 0) { anyVis = true; break; }
}
ratio = anyVis ? 1f : 0f;
} }
} }
// 空bar暗色背景(满bar不在此处,由纹理裁剪提供) // 从纹理 Y=202 裁剪满亮条,按高度百分比叠加
g.fill(bx, by, bx + VIS_BAR_W, by + VIS_BAR_H, 0x77222222);
// 从纹理Y=202处裁剪满bar,按高度百分比叠加
int fillHeight = Math.round(VIS_BAR_H * ratio); int fillHeight = Math.round(VIS_BAR_H * ratio);
if (fillHeight > 0) { if (fillHeight > 0) {
int srcX = VIS_BAR_X + i * VIS_BAR_STEP; int srcX = VIS_BAR_X + i * VIS_BAR_STEP;
int srcY = VIS_BAR_Y + (VIS_BAR_H - fillHeight); int srcY = VIS_FULL_Y + (VIS_BAR_H - fillHeight);
g.blit(TEX, bx, by + (VIS_BAR_H - fillHeight), srcX, srcY, VIS_BAR_W, fillHeight); int dstY = by + (VIS_BAR_H - fillHeight);
g.blit(TEX, bx, dstY, srcX, srcY, VIS_BAR_W, fillHeight);
} }
// bar边框 // bar 边框
g.fill(bx - 1, by - 1, bx, by + VIS_BAR_H + 1, 0xAA000000); g.fill(bx - 1, by - 1, bx, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx + VIS_BAR_W, by - 1, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000); g.fill(bx + VIS_BAR_W, by - 1, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx - 1, by - 1, bx + VIS_BAR_W + 1, by, 0xAA000000); g.fill(bx - 1, by - 1, bx + VIS_BAR_W + 1, by, 0xAA000000);
@@ -47,7 +47,7 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
if (cachedAnchorY < 0) { if (cachedAnchorY < 0) {
for (var slot : this.menu.slots) { for (var slot : this.menu.slots) {
// CRAFTING_RESULT 槽在 style JSON 中 bottom:140 // CRAFTING_RESULT 槽在 style JSON 中 bottom:140
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) { if (slot instanceof AppEngCraftingSlot) {
cachedAnchorY = slot.y; cachedAnchorY = slot.y;
break; break;
} }
@@ -383,11 +383,11 @@ public final class RecipeRegistration {
return new ItemStack(s.get()); return new ItemStack(s.get());
} }
private static ItemStack tc(String name) { private static ItemStack tc(String name) {
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get( return new ItemStack(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", name))); ResourceLocation.fromNamespaceAndPath("thaumcraft", name)));
} }
private static ItemStack ae(String name) { private static ItemStack ae(String name) {
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get( return new ItemStack(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("ae2", name))); ResourceLocation.fromNamespaceAndPath("ae2", name)));
} }
} }
@@ -87,7 +87,7 @@ public class PseudoResearchItem extends ResearchItem {
} else { } else {
// 最终备选:ThE 研究 tab 图标 // 最终备选:ThE 研究 tab 图标
pseudo = new PseudoResearchItem(pseudoKey, category, column, row, pseudo = new PseudoResearchItem(pseudoKey, category, column, row,
ResourceLocation.fromNamespaceAndPath(thaumicenergistics.ThaumicEnergistics.MODID, "textures/research/tab_icon.png"), ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "textures/research/tab_icon.png"),
realKey, realCategory); realKey, realCategory);
} }
@@ -36,7 +36,7 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
private final int totalTypes; private final int totalTypes;
private final double idleDrain; private final double idleDrain;
public ItemEssentiaCell(Item.Properties props, String tier, int kilobytes, int bytesPerType, int totalTypes, double idleDrain) { public ItemEssentiaCell(Properties props, String tier, int kilobytes, int bytesPerType, int totalTypes, double idleDrain) {
super(props.stacksTo(1)); super(props.stacksTo(1));
this.tier = tier; this.tier = tier;
this.totalBytes = kilobytes * 1024; this.totalBytes = kilobytes * 1024;
@@ -77,7 +77,7 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
@Override @Override
public boolean isBlackListed(ItemStack cellItem, appeng.api.stacks.AEKey requestedAddition) { public boolean isBlackListed(ItemStack cellItem, appeng.api.stacks.AEKey requestedAddition) {
if (requestedAddition instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey) { if (requestedAddition instanceof AEssentiaKey) {
return false; return false;
} }
return true; return true;
@@ -208,19 +208,19 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
} }
// ===== 工厂方法 ===== // ===== 工厂方法 =====
public static ItemEssentiaCell create1k(Item.Properties props) { public static ItemEssentiaCell create1k(Properties props) {
return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5); return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5);
} }
public static ItemEssentiaCell create4k(Item.Properties props) { public static ItemEssentiaCell create4k(Properties props) {
return new ItemEssentiaCell(props, "4k", 4, 8, ThEConfig.CELL_MAX_TYPES, 1.0); return new ItemEssentiaCell(props, "4k", 4, 8, ThEConfig.CELL_MAX_TYPES, 1.0);
} }
public static ItemEssentiaCell create16k(Item.Properties props) { public static ItemEssentiaCell create16k(Properties props) {
return new ItemEssentiaCell(props, "16k", 16, 8, ThEConfig.CELL_MAX_TYPES, 1.5); return new ItemEssentiaCell(props, "16k", 16, 8, ThEConfig.CELL_MAX_TYPES, 1.5);
} }
public static ItemEssentiaCell create64k(Item.Properties props) { public static ItemEssentiaCell create64k(Properties props) {
return new ItemEssentiaCell(props, "64k", 64, 8, ThEConfig.CELL_MAX_TYPES, 2.0); return new ItemEssentiaCell(props, "64k", 64, 8, ThEConfig.CELL_MAX_TYPES, 2.0);
} }
public static ItemEssentiaCell createCreative(Item.Properties props) { public static ItemEssentiaCell createCreative(Properties props) {
return new ItemEssentiaCell(props, "creative", Integer.MAX_VALUE / 1024, 8, 63, 0.0); return new ItemEssentiaCell(props, "creative", Integer.MAX_VALUE / 1024, 8, 63, 0.0);
} }
} }
@@ -17,7 +17,7 @@ public class ItemGolemWirelessBackpack extends Item {
public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler(); public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler();
public ItemGolemWirelessBackpack() { public ItemGolemWirelessBackpack() {
super(new Item.Properties().stacksTo(1).durability(0)); super(new Properties().stacksTo(1).durability(0));
} }
@Override @Override
@@ -16,7 +16,7 @@ import java.util.List;
public class PartItemEssentiaExportBus extends Item implements IPartItem<EssentiaExportBusPart> { public class PartItemEssentiaExportBus extends Item implements IPartItem<EssentiaExportBusPart> {
public static PartItemEssentiaExportBus create() { public static PartItemEssentiaExportBus create() {
return new PartItemEssentiaExportBus(new Item.Properties().stacksTo(64)); return new PartItemEssentiaExportBus(new Properties().stacksTo(64));
} }
public PartItemEssentiaExportBus(Properties properties) { public PartItemEssentiaExportBus(Properties properties) {
@@ -16,7 +16,7 @@ import java.util.List;
public class PartItemEssentiaImportBus extends Item implements IPartItem<EssentiaImportBusPart> { public class PartItemEssentiaImportBus extends Item implements IPartItem<EssentiaImportBusPart> {
public static PartItemEssentiaImportBus create() { public static PartItemEssentiaImportBus create() {
return new PartItemEssentiaImportBus(new Item.Properties().stacksTo(64)); return new PartItemEssentiaImportBus(new Properties().stacksTo(64));
} }
public PartItemEssentiaImportBus(Properties properties) { public PartItemEssentiaImportBus(Properties properties) {
@@ -16,7 +16,7 @@ import java.util.List;
public class PartItemEssentiaStorageBus extends Item implements IPartItem<EssentiaStorageBusPart> { public class PartItemEssentiaStorageBus extends Item implements IPartItem<EssentiaStorageBusPart> {
public static PartItemEssentiaStorageBus create() { public static PartItemEssentiaStorageBus create() {
return new PartItemEssentiaStorageBus(new Item.Properties().stacksTo(64)); return new PartItemEssentiaStorageBus(new Properties().stacksTo(64));
} }
public PartItemEssentiaStorageBus(Properties properties) { public PartItemEssentiaStorageBus(Properties properties) {
@@ -0,0 +1,45 @@
package thaumicenergistics.common.network;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerDistillationPatternEncoder;
/**
* 客户端→服务端 网络包:蒸馏编码台设置源物品(JEI 拖拽)。
* 源物品槽是真实 SlotItemHandler(非 ghost),拖拽需服务端权威写入 encoder 槽 0 + 刷新 aspects。
*/
public record DistillationEncoderSetSourceC2SPacket(
int containerId,
ItemStack stack
) implements CustomPacketPayload {
public static final Type<DistillationEncoderSetSourceC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("de_set_source"));
public static final StreamCodec<RegistryFriendlyByteBuf, DistillationEncoderSetSourceC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, DistillationEncoderSetSourceC2SPacket::containerId,
ItemStack.OPTIONAL_STREAM_CODEC, DistillationEncoderSetSourceC2SPacket::stack,
DistillationEncoderSetSourceC2SPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
/** 服务端处理:找到对应菜单,写入源物品槽并刷新 aspects。 */
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof ContainerDistillationPatternEncoder menu) {
menu.setSourceItemOnServer(this.stack);
}
});
}
}
@@ -29,6 +29,13 @@ public final class ThENetworking {
(pkt, ctx) -> pkt.handleOnServer(ctx) (pkt, ctx) -> pkt.handleOnServer(ctx)
); );
// DistillationEncoderSetSourceC2SPacket — C2S(蒸馏编码台设置源物品——JEI 拖拽)
registrar.playToServer(
DistillationEncoderSetSourceC2SPacket.TYPE,
DistillationEncoderSetSourceC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// CellWorkbenchC2SPacket — C2S(客户端→服务端,源质元件工作台分区操作) // CellWorkbenchC2SPacket — C2S(客户端→服务端,源质元件工作台分区操作)
registrar.playToServer( registrar.playToServer(
CellWorkbenchC2SPacket.TYPE, CellWorkbenchC2SPacket.TYPE,
@@ -19,8 +19,8 @@ public class TransportPermissions implements IThETransportPermissions {
@Override @Override
public <T extends BlockEntity> boolean addTileToInject(Class<T> c, int cap) { return tileInject.putIfAbsent(c, cap) == null; } public <T extends BlockEntity> boolean addTileToInject(Class<T> c, int cap) { return tileInject.putIfAbsent(c, cap) == null; }
@Override @Override
public java.util.OptionalLong getCapacityForTile(Class<? extends BlockEntity> c) { public OptionalLong getCapacityForTile(Class<? extends BlockEntity> c) {
Integer v = tileExtract.get(c); return v != null ? java.util.OptionalLong.of(v) : java.util.OptionalLong.empty(); Integer v = tileExtract.get(c); return v != null ? OptionalLong.of(v) : OptionalLong.empty();
} }
@Override @Override
public boolean canExtract(BlockEntity tile) { return tile != null && tileExtract.containsKey(tile.getClass()); } public boolean canExtract(BlockEntity tile) { return tile != null && tileExtract.containsKey(tile.getClass()); }
@@ -75,7 +75,7 @@ public class TileArcaneAssembler extends ThETileBase
private static final double ACTIVE_POWER = 1.5; private static final double ACTIVE_POWER = 1.5;
private static final double WARP_POWER_PERCENT = 0.15; private static final double WARP_POWER_PERCENT = 0.15;
private static final int MAX_SPEED_UPGRADES = 4; private static final int MAX_SPEED_UPGRADES = 4;
private static final int VIS_REPLENISH_INTERVAL = 5; private static final int VIS_REPLENISH_INTERVAL = 20;
/** 每次实际 vis 变化累计达到该值才调用一次 setChanged(约 4×5=20 tick),避免合成期间 chunk 持续处于 unsaved 状态。 */ /** 每次实际 vis 变化累计达到该值才调用一次 setChanged(约 4×5=20 tick),避免合成期间 chunk 持续处于 unsaved 状态。 */
private static final int SET_CHANGED_INTERVAL = 4; private static final int SET_CHANGED_INTERVAL = 4;
@@ -141,7 +141,7 @@ public class TileArcaneAssembler extends ThETileBase
super(ModBlockEntities.ARCANE_ASSEMBLER.get(), pos, state); super(ModBlockEntities.ARCANE_ASSEMBLER.get(), pos, state);
mainNode = GridHelper.createManagedNode(this, nodeListener) mainNode = GridHelper.createManagedNode(this, nodeListener)
.setVisualRepresentation(thaumicenergistics.init.ModItems.ARCANE_ASSEMBLER_ITEM.get()) .setVisualRepresentation(ModItems.ARCANE_ASSEMBLER_ITEM.get())
.setInWorldNode(true) .setInWorldNode(true)
.setTagName("proxy") .setTagName("proxy")
.setFlags(GridFlags.REQUIRE_CHANNEL) .setFlags(GridFlags.REQUIRE_CHANNEL)
@@ -296,13 +296,13 @@ public class TileArcaneAssembler extends ThETileBase
} }
/** /**
* replenishVis 抽到 vis 时调用:累计变化次数,每 SET_CHANGED_INTERVAL 次才标记一次 chunk dirty * replenishVis 抽到 vis 时调用:累计变化次数,每 SET_CHANGED_INTERVAL 次才 markForUpdate 一次
* 降低合成期间持续磁盘写放大;storedVis 最终仍会在合成完成路径(craftingTick)或区块保存时落盘 * markForUpdate = setChanged + sendBlockUpdated(同步 storedVis 到客户端 BEGUI 进度条依赖客户端 storedVis
*/ */
private void maybeMarkVisDirty() { private void maybeMarkVisDirty() {
if (++visSetChangedCounter >= SET_CHANGED_INTERVAL) { if (++visSetChangedCounter >= SET_CHANGED_INTERVAL) {
visSetChangedCounter = 0; visSetChangedCounter = 0;
setChanged(); markForUpdate();
} }
} }
@@ -360,6 +360,8 @@ public class TileArcaneAssembler extends ThETileBase
if (extracted >= need * 0.9) { if (extracted >= need * 0.9) {
craftTickCounter += ticksSinceLast; craftTickCounter += ticksSinceLast;
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need); ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need);
// 节流同步合成进度到客户端(GUI 第7格进度条动画)
maybeMarkVisDirty();
} else { } else {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need); ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need);
} }
@@ -375,7 +377,7 @@ public class TileArcaneAssembler extends ThETileBase
int current = storedVis.getAmount(aspect); int current = storedVis.getAmount(aspect);
storedVis.reduce(aspect, Math.min(requiredCvis, current)); storedVis.reduce(aspect, Math.min(requiredCvis, current));
} }
setChanged(); maybeMarkVisDirty();
} }
private boolean hasEnoughVisForCraft() { private boolean hasEnoughVisForCraft() {
@@ -512,7 +514,7 @@ public class TileArcaneAssembler extends ThETileBase
@Override @Override
public PatternContainerGroup getCraftingMachineInfo() { public PatternContainerGroup getCraftingMachineInfo() {
ItemStack icon = new ItemStack(thaumicenergistics.init.ModItems.ARCANE_ASSEMBLER_ITEM.get()); ItemStack icon = new ItemStack(ModItems.ARCANE_ASSEMBLER_ITEM.get());
return new PatternContainerGroup( return new PatternContainerGroup(
AEItemKey.of(icon), AEItemKey.of(icon),
net.minecraft.network.chat.Component.translatable("block.thaumicenergistics.arcane_assembler"), net.minecraft.network.chat.Component.translatable("block.thaumicenergistics.arcane_assembler"),
@@ -575,7 +577,7 @@ public class TileArcaneAssembler extends ThETileBase
*/ */
public void onMemoryCardActivate(net.minecraft.world.entity.player.Player player, public void onMemoryCardActivate(net.minecraft.world.entity.player.Player player,
appeng.api.implementations.items.IMemoryCard memoryCard, appeng.api.implementations.items.IMemoryCard memoryCard,
net.minecraft.world.item.ItemStack heldItem) { ItemStack heldItem) {
Short freq = heldItem.get(appeng.api.ids.AEComponents.EXPORTED_P2P_FREQUENCY); Short freq = heldItem.get(appeng.api.ids.AEComponents.EXPORTED_P2P_FREQUENCY);
if (freq != null) { if (freq != null) {
// 卡上带 P2P 频率(从 vis_interface shift+右键复制来的)→ 设为 vis 源 // 卡上带 P2P 频率(从 vis_interface shift+右键复制来的)→ 设为 vis 源
@@ -643,6 +645,9 @@ public class TileArcaneAssembler extends ThETileBase
if (level != null) { if (level != null) {
setChanged(); setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3); level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
// 关键:blockEntityChanged 触发 BE 数据(getUpdateTag)同步到客户端。
// setChanged 只标记区块保存、sendBlockUpdated 只触发重渲染——缺这个 GUI 永远拿不到 storedVis/isCrafting。
level.blockEntityChanged(worldPosition);
} }
} }
@@ -37,14 +37,14 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
} }
/** getEssentiaList 短时缓存:TC 管道 suction 每 tick 调 getEssentiaType,避免每 tick 全量遍历。 */ /** getEssentiaList 短时缓存:TC 管道 suction 每 tick 调 getEssentiaType,避免每 tick 全量遍历。 */
private List<IAspectStack> cachedEssentiaList = java.util.List.of(); private List<IAspectStack> cachedEssentiaList = List.of();
private int essentiaListCacheTimer = 0; private int essentiaListCacheTimer = 0;
private static final int ESSENTIA_LIST_CACHE_TICKS = 20; private static final int ESSENTIA_LIST_CACHE_TICKS = 20;
private List<thaumicenergistics.api.storage.IAspectStack> getCachedEssentiaList() { private List<IAspectStack> getCachedEssentiaList() {
if (essentiaListCacheTimer <= 0) { if (essentiaListCacheTimer <= 0) {
essentiaListCacheTimer = ESSENTIA_LIST_CACHE_TICKS; essentiaListCacheTimer = ESSENTIA_LIST_CACHE_TICKS;
cachedEssentiaList = essentiaGrid != null ? essentiaGrid.getEssentiaList() : java.util.List.of(); cachedEssentiaList = essentiaGrid != null ? essentiaGrid.getEssentiaList() : List.of();
} }
return cachedEssentiaList; return cachedEssentiaList;
} }
@@ -39,12 +39,13 @@ public final class ModBlocks {
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide()); return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
} }
if (!level.isClientSide) { if (!level.isClientSide) {
player.openMenu(state.getMenuProvider(level, pos)); ((net.neoforged.neoforge.common.extensions.IPlayerExtension) player)
.openMenu(state.getMenuProvider(level, pos), pos);
} }
return net.minecraft.world.ItemInteractionResult.SUCCESS; return net.minecraft.world.ItemInteractionResult.SUCCESS;
} }
@Override @Override
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) { protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
TileArcaneAssembler tile = (TileArcaneAssembler) level.getBlockEntity(pos); TileArcaneAssembler tile = (TileArcaneAssembler) level.getBlockEntity(pos);
return new SimpleMenuProvider( return new SimpleMenuProvider(
(id, inv, p) -> new thaumicenergistics.common.container.ContainerArcaneAssembler(id, inv, tile), (id, inv, p) -> new thaumicenergistics.common.container.ContainerArcaneAssembler(id, inv, tile),
@@ -68,7 +69,7 @@ public final class ModBlocks {
return net.minecraft.world.ItemInteractionResult.SUCCESS; return net.minecraft.world.ItemInteractionResult.SUCCESS;
} }
@Override @Override
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) { protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
TileKnowledgeInscriber tile = (TileKnowledgeInscriber) level.getBlockEntity(pos); TileKnowledgeInscriber tile = (TileKnowledgeInscriber) level.getBlockEntity(pos);
return new SimpleMenuProvider( return new SimpleMenuProvider(
(id, inv, p) -> new thaumicenergistics.common.container.ContainerKnowledgeInscriber(id, inv, tile), (id, inv, p) -> new thaumicenergistics.common.container.ContainerKnowledgeInscriber(id, inv, tile),
@@ -91,7 +92,7 @@ public final class ModBlocks {
return net.minecraft.world.ItemInteractionResult.SUCCESS; return net.minecraft.world.ItemInteractionResult.SUCCESS;
} }
@Override @Override
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) { protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
TileDistillationPatternEncoder tile = (TileDistillationPatternEncoder) level.getBlockEntity(pos); TileDistillationPatternEncoder tile = (TileDistillationPatternEncoder) level.getBlockEntity(pos);
return new SimpleMenuProvider( return new SimpleMenuProvider(
(id, inv, p) -> new thaumicenergistics.common.container.ContainerDistillationPatternEncoder(id, inv, tile), (id, inv, p) -> new thaumicenergistics.common.container.ContainerDistillationPatternEncoder(id, inv, tile),
@@ -109,7 +110,7 @@ public final class ModBlocks {
return net.minecraft.world.ItemInteractionResult.SUCCESS; return net.minecraft.world.ItemInteractionResult.SUCCESS;
} }
@Override @Override
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) { protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
net.minecraft.world.level.block.entity.BlockEntity be = level.getBlockEntity(pos); net.minecraft.world.level.block.entity.BlockEntity be = level.getBlockEntity(pos);
if (be instanceof TileEssentiaCellWorkbench workbench) { if (be instanceof TileEssentiaCellWorkbench workbench) {
return new SimpleMenuProvider((id, inv, p) -> new thaumicenergistics.common.container.ContainerEssentiaCellWorkbench(id, inv, workbench), return new SimpleMenuProvider((id, inv, p) -> new thaumicenergistics.common.container.ContainerEssentiaCellWorkbench(id, inv, workbench),
@@ -45,11 +45,11 @@ public class ArcaneCraftingTransferInfo
@Override @Override
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) { public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
return menu.getSlots(appeng.menu.SlotSemantics.CRAFTING_GRID); return menu.getSlots(SlotSemantics.CRAFTING_GRID);
} }
@Override @Override
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) { public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
return menu.getSlots(appeng.menu.SlotSemantics.PLAYER_INVENTORY); return menu.getSlots(SlotSemantics.PLAYER_INVENTORY);
} }
} }
@@ -0,0 +1,55 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.world.item.ItemStack;
import mezz.jei.api.gui.handlers.IGhostIngredientHandler;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.container.GuiDistillationPatternEncoder;
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
/**
* 蒸馏编码台 JEI 拖拽:从 JEI 拉取物品到源物品槽(15,69)→ C2S 设置源物品。
*/
public class DistillationEncoderGhostIngredientHandler
implements IGhostIngredientHandler<GuiDistillationPatternEncoder> {
/** 与 ContainerDistillationPatternEncoder.SLOT_SOURCE_X/Y 一致(15, 69)。 */
private static final int SOURCE_SLOT_X = 15;
private static final int SOURCE_SLOT_Y = 69;
@Override
public <I> List<Target<I>> getTargetsTyped(GuiDistillationPatternEncoder gui, ITypedIngredient<I> ingredient, boolean doStart) {
List<Target<I>> targets = new ArrayList<>();
if (!(ingredient.getIngredient() instanceof ItemStack)) {
return targets;
}
int wx = gui.getGuiLeft() + SOURCE_SLOT_X;
int wy = gui.getGuiTop() + SOURCE_SLOT_Y;
targets.add(new Target<>() {
@Override
public Rect2i getArea() {
return new Rect2i(wx, wy, 16, 16);
}
@Override
public void accept(I ingredient) {
if (ingredient instanceof ItemStack stack) {
PacketDistributor.sendToServer(new DistillationEncoderSetSourceC2SPacket(
gui.getMenu().containerId, stack.copyWithCount(1)));
}
}
});
return targets;
}
@Override
public void onComplete() {
}
}
@@ -0,0 +1,93 @@
package thaumicenergistics.integration.jei;
import java.util.List;
import java.util.Optional;
import net.minecraft.core.NonNullList;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandler;
import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumcraft.integration.jei.JeiArcaneRecipe;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.common.container.ContainerKnowledgeInscriber;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
import thaumicenergistics.init.ModMenuTypes;
/**
* 知识记录仪 JEI 奥术配方 "+" 一键放置:
* 点 "+" → 把配方材料填入 3×3 ghost 合成网格(有序按位置 / 无序填前 N 格)。
* 复用 KnowledgeInscriberGhostSlotPacket 同步服务端(客户端同时刷新产物)。
*/
public class KnowledgeInscriberArcaneRecipeTransfer
implements IRecipeTransferHandler<ContainerKnowledgeInscriber, JeiArcaneRecipe> {
@SuppressWarnings("unused")
private final IRecipeTransferHandlerHelper helper;
public KnowledgeInscriberArcaneRecipeTransfer(IRecipeTransferHandlerHelper helper) {
this.helper = helper;
}
@Override
public Class<? extends ContainerKnowledgeInscriber> getContainerClass() {
return ContainerKnowledgeInscriber.class;
}
@Override
public Optional<MenuType<ContainerKnowledgeInscriber>> getMenuType() {
return Optional.of(ModMenuTypes.KNOWLEDGE_INSCRIBER.get());
}
@Override
public RecipeType<JeiArcaneRecipe> getRecipeType() {
return ThaumcraftJeiPlugin.ARCANE;
}
@Override
public IRecipeTransferError transferRecipe(
ContainerKnowledgeInscriber menu,
JeiArcaneRecipe recipe,
mezz.jei.api.gui.ingredient.IRecipeSlotsView display,
net.minecraft.world.entity.player.Player player,
boolean maxTransfer,
boolean doTransfer) {
// 预览:始终可转移(ghost 槽不消耗物品)
if (!doTransfer) {
return null;
}
// 构建 9 格模板(有序配方 inputs 为 3×3 展开含空位;无序配方为压缩列表——填前 N 格)
NonNullList<ItemStack> template = NonNullList.withSize(9, ItemStack.EMPTY);
List<List<ItemStack>> inputs = recipe.inputs();
for (int i = 0; i < template.size() && i < inputs.size(); i++) {
List<ItemStack> variants = inputs.get(i);
if (variants != null && !variants.isEmpty()) {
template.set(i, variants.get(0).copyWithCount(1));
}
}
// 按 ghost 槽顺序填入 + C2S 同步服务端 + 客户端刷新产物
List<GhostSlot> ghostSlots = menu.slots.stream()
.filter(s -> s instanceof GhostSlot)
.map(s -> (GhostSlot) s)
.toList();
for (int i = 0; i < template.size(); i++) {
ItemStack stack = template.get(i);
if (stack.isEmpty() || i >= ghostSlots.size()) continue;
ghostSlots.get(i).set(stack);
PacketDistributor.sendToServer(
new KnowledgeInscriberGhostSlotPacket(menu.containerId, i, stack));
}
menu.onCraftingChangedClient();
return null;
}
}
@@ -0,0 +1,65 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.world.item.ItemStack;
import mezz.jei.api.gui.handlers.IGhostIngredientHandler;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.container.GuiKnowledgeInscriber;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
/**
* 知识记录仪 JEI 拖拽:从 JEI 拉取物品到 3×3 合成网格(ghost 槽)。
* 复用已有的 GhostSlot + KnowledgeInscriberGhostSlotPacket(客户端 set + C2S 同步服务端)。
*/
public class KnowledgeInscriberGhostIngredientHandler
implements IGhostIngredientHandler<GuiKnowledgeInscriber> {
@Override
public <I> List<Target<I>> getTargetsTyped(GuiKnowledgeInscriber gui, ITypedIngredient<I> ingredient, boolean doStart) {
List<Target<I>> targets = new ArrayList<>();
if (!(ingredient.getIngredient() instanceof ItemStack)) {
return targets;
}
for (var slot : gui.getMenu().slots) {
if (slot instanceof GhostSlot ghostSlot) {
int fx = slot.x;
int fy = slot.y;
targets.add(new Target<>() {
@Override
public Rect2i getArea() {
return new Rect2i(gui.getGuiLeft() + fx, gui.getGuiTop() + fy, 16, 16);
}
@Override
public void accept(I ingredient) {
if (ingredient instanceof ItemStack stack) {
ItemStack s = stack.copyWithCount(1);
ghostSlot.set(s);
// 与手动点击一致:客户端刷新产物计算 + C2S 同步服务端
if (gui.getMenu() instanceof thaumicenergistics.common.container.ContainerKnowledgeInscriber menu) {
menu.onCraftingChangedClient();
}
int gridIndex = gui.getMenu() instanceof thaumicenergistics.common.container.ContainerKnowledgeInscriber m
? m.getGhostGridIndex(ghostSlot) : -1;
PacketDistributor.sendToServer(
new KnowledgeInscriberGhostSlotPacket(gui.getMenu().containerId, gridIndex, s));
}
}
});
}
}
return targets;
}
@Override
public void onComplete() {
}
}
@@ -0,0 +1,62 @@
package thaumicenergistics.integration.jei;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.transfer.IRecipeTransferInfo;
import thaumcraft.integration.jei.JeiArcaneRecipe;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.common.container.ContainerKnowledgeInscriber;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.init.ModMenuTypes;
/**
* 知识记录仪 JEI 奥术配方转移槽位映射(让 JEI 显示 "+" 按钮)。
* recipeSlots = 3×3 ghost 合成网格;inventorySlots = 玩家背包。
*/
public class KnowledgeInscriberTransferInfo
implements IRecipeTransferInfo<ContainerKnowledgeInscriber, JeiArcaneRecipe> {
@Override
public Class<? extends ContainerKnowledgeInscriber> getContainerClass() {
return ContainerKnowledgeInscriber.class;
}
@Override
public Optional<MenuType<ContainerKnowledgeInscriber>> getMenuType() {
return Optional.of(ModMenuTypes.KNOWLEDGE_INSCRIBER.get());
}
@Override
public RecipeType<JeiArcaneRecipe> getRecipeType() {
return ThaumcraftJeiPlugin.ARCANE;
}
@Override
public boolean canHandle(ContainerKnowledgeInscriber menu, JeiArcaneRecipe recipe) {
return true;
}
@Override
public List<Slot> getRecipeSlots(ContainerKnowledgeInscriber menu, JeiArcaneRecipe recipe) {
List<Slot> slots = new ArrayList<>();
for (Slot slot : menu.slots) {
if (slot instanceof GhostSlot) {
slots.add(slot);
}
}
return slots;
}
@Override
public List<Slot> getInventorySlots(ContainerKnowledgeInscriber menu, JeiArcaneRecipe recipe) {
// 玩家背包槽先于机器槽加入(0-35)
return menu.slots.subList(0, 36);
}
}
@@ -90,11 +90,24 @@ public class ThEJeiPlugin implements IModPlugin {
// 注册 IRecipeTransferHandler 让点击 "+" 时走 ME 网络 // 注册 IRecipeTransferHandler 让点击 "+" 时走 ME 网络
registration.addRecipeTransferHandler(new ThEUseCraftingRecipeTransfer(registration.getTransferHelper()), CRAFTING); registration.addRecipeTransferHandler(new ThEUseCraftingRecipeTransfer(registration.getTransferHelper()), CRAFTING);
registration.addRecipeTransferHandler(new ThEUseArcaneRecipeTransfer(registration.getTransferHelper()), ThaumcraftJeiPlugin.ARCANE); registration.addRecipeTransferHandler(new ThEUseArcaneRecipeTransfer(registration.getTransferHelper()), ThaumcraftJeiPlugin.ARCANE);
// 知识记录仪:奥术配方 "+" 一键放置到 3×3 ghost 合成网格
registration.addRecipeTransferHandler(new KnowledgeInscriberTransferInfo());
registration.addRecipeTransferHandler(
new KnowledgeInscriberArcaneRecipeTransfer(registration.getTransferHelper()), ThaumcraftJeiPlugin.ARCANE);
} }
@Override @Override
public void registerGuiHandlers(IGuiHandlerRegistration registration) { public void registerGuiHandlers(IGuiHandlerRegistration registration) {
// 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能) // 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能)
registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler()); registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler());
// 知识记录仪:从 JEI 拉取物品到 3×3 合成网格(复用 GhostSlot + KnowledgeInscriberGhostSlotPacket
registration.addGhostIngredientHandler(
thaumicenergistics.common.container.GuiKnowledgeInscriber.class,
new KnowledgeInscriberGhostIngredientHandler());
// 蒸馏编码台:从 JEI 拉取物品到源物品槽(C2S 设置源物品)
registration.addGhostIngredientHandler(
thaumicenergistics.common.container.GuiDistillationPatternEncoder.class,
new DistillationEncoderGhostIngredientHandler());
} }
} }
@@ -12,7 +12,7 @@
"bottom": 140 "bottom": 140
}, },
"STORAGE": { "STORAGE": {
"left": 136, "left": 134,
"bottom": 163 "bottom": 163
} }
}, },
@@ -1 +1,6 @@
{"parent":"minecraft:block/cube_all","textures":{"all":"thaumicenergistics:block/infusion_provider"}} {
"parent": "minecraft:block/cube_all",
"textures": {
"all": "thaumicenergistics:block/infusion_provider"
}
}