This commit is contained in:
1820982382
2026-08-01 16:42:53 +08:00
parent e2ab0fb78b
commit 7b341980bd
322 changed files with 15941 additions and 0 deletions
+74
View File
@@ -0,0 +1,74 @@
plugins {
id 'net.neoforged.moddev' version '2.0.78'
id 'maven-publish'
}
repositories {
mavenCentral()
def localRepository = mavenLocal()
remove(localRepository)
add(0, localRepository)
// CurseMaven for Jade and other mods
maven {
url "https://cursemaven.com"
content {
includeGroup "curse.maven"
}
}
// Modrinth for Jade
maven {
url "https://api.modrinth.com/maven"
}
}
version = mod_version
group = mod_group
var modName = mod_name
var mcVersion = minecraft_version
base { archivesName = "${modName}-${mcVersion}-neoforge" }
java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
withSourcesJar()
}
neoForge {
version = neoforge_version
runs {
client { client() }
server { server() }
}
mods { "${mod_id}" { sourceSet sourceSets.main } }
}
dependencies {
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api"
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17" // 加这行保留!!让AE2内部类可编译
compileOnly files('library/thaumcraft-0.2.2.34-port.1.jar')
compileOnly files('library/jei-1.21.1-neoforge-19.39.0.368.jar')
compileOnly files('library/ae2jeiintegration-1.2.1.jar')
compileOnly "maven.modrinth:jade:${jade_version}"
}
tasks.withType(JavaCompile).configureEach {
it.options.encoding = 'UTF-8'
it.options.release = 21
}
var modVersion = mod_version
def loaderRange = project.findProperty('loader_version_range') ?: '[4,)'
def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)'
def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)'
processResources {
filesMatching('META-INF/neoforge.mods.toml') {
expand([
'version': modVersion, 'loader_version_range': loaderRange,
'neoforge_version_range': neoforgeRange, 'minecraft_version_range': mcRange
])
}
}
jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE }
+18
View File
@@ -0,0 +1,18 @@
# Project
mod_version=2.0.1-alpha
mod_group=thaumicenergistics
# Mod
mod_name=ThaumicEnergistics
mod_id=thaumicenergistics
mod_author=Nividica & Community
# Shared (must match root gradle.properties)
minecraft_version=1.21.1
neoforge_version=21.1.234
# Dependency versions
ae2_version=19.2.17
jei_version=19.39.0.368
thaumcraft_version=0.2.2.34-port.1
jade_version=15.9.1+neoforge
Binary file not shown.
+8
View File
@@ -0,0 +1,8 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+14
View File
@@ -0,0 +1,14 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
maven { url = 'https://maven.neoforged.net/releases' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
include("thaumicenergistics")
project(":thaumicenergistics").projectDir = file("modules/thaumicenergistics-neo")
@@ -0,0 +1,208 @@
package thaumicenergistics;
import appeng.api.features.GridLinkables;
import appeng.api.parts.PartModels;
import appeng.api.stacks.AEKeyType;
import appeng.api.stacks.AEKeyTypesInternal;
import appeng.api.upgrades.Upgrades;
import appeng.client.gui.style.StyleManager;
import appeng.core.definitions.AEItems;
import appeng.core.localization.GuiText;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import appeng.api.AECapabilities;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import thaumicenergistics.common.container.*;
import thaumicenergistics.common.features.FeatureRegistry;
import thaumicenergistics.common.features.RecipeRegistration;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.common.integration.appeng.ThEAppliedEnergistics;
import thaumicenergistics.common.integration.tc.ThEThaumcraft;
import thaumicenergistics.common.items.ItemGolemWirelessBackpack;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
import thaumicenergistics.common.parts.EssentiaExportBusPart;
import thaumicenergistics.common.parts.EssentiaImportBusPart;
import thaumicenergistics.common.parts.EssentiaLevelEmitterPart;
import thaumicenergistics.common.parts.EssentiaStorageBusPart;
import thaumicenergistics.common.parts.EssentiaTerminalPart;
import thaumicenergistics.common.parts.VisInterfacePart;
import thaumicenergistics.init.*;
@Mod(ThaumicEnergistics.MODID)
public final class ThaumicEnergistics {
public static final String MODID = "thaumicenergistics";
public static final Logger LOG = LoggerFactory.getLogger("ThaumicEnergistics");
public ThaumicEnergistics(IEventBus modBus) {
LOG.info("ThaumicEnergistics loading...");
ModBlocks.REGISTRY.register(modBus);
ModItems.REGISTRY.register(modBus);
ModBlockEntities.REGISTRY.register(modBus);
ModMenuTypes.REGISTRY.register(modBus);
ModCreativeTab.REGISTRY.register(modBus);
modBus.addListener(this::registerScreens);
modBus.addListener(this::commonSetup);
modBus.addListener(this::registerKeyTypes);
modBus.addListener(this::registerUpgrades);
if (net.neoforged.fml.loading.FMLLoader.getDist().isClient()) {
modBus.addListener(thaumicenergistics.client.ClientInit::onClientSetup);
}
ThEAppliedEnergistics.init();
appeng.api.networking.GridServices.register(
thaumicenergistics.api.grid.IEssentiaGrid.class,
thaumicenergistics.common.grid.EssentiaGridService.class
);
thaumicenergistics.common.network.ThENetworking.init(modBus);
modBus.addListener(this::registerCapabilities);
PartModels.registerModels(
EssentiaImportBusPart.RL_BASE, EssentiaImportBusPart.RL_OFF,
EssentiaImportBusPart.RL_HAS_CHANNEL, EssentiaImportBusPart.RL_ON
);
PartModels.registerModels(
EssentiaExportBusPart.RL_BASE, EssentiaExportBusPart.RL_OFF,
EssentiaExportBusPart.RL_HAS_CHANNEL, EssentiaExportBusPart.RL_ON
);
PartModels.registerModels(
EssentiaStorageBusPart.RL_BASE, EssentiaStorageBusPart.RL_OFF,
EssentiaStorageBusPart.RL_HAS_CHANNEL, EssentiaStorageBusPart.RL_ON
);
PartModels.registerModels(
EssentiaTerminalPart.RL_BASE, EssentiaTerminalPart.RL_OFF, EssentiaTerminalPart.RL_ON
);
PartModels.registerModels(
ArcaneCraftingTerminalPart.RL_BASE, ArcaneCraftingTerminalPart.RL_OFF,
ArcaneCraftingTerminalPart.RL_HAS_CHANNEL, ArcaneCraftingTerminalPart.RL_ON
);
PartModels.registerModels(
EssentiaLevelEmitterPart.RL_BASE_OFF, EssentiaLevelEmitterPart.RL_BASE_ON,
EssentiaLevelEmitterPart.RL_STATUS_HAS_CHANNEL, EssentiaLevelEmitterPart.RL_STATUS_ON,
EssentiaLevelEmitterPart.RL_STATUS_OFF
);
PartModels.registerModels(VisInterfacePart.MODEL_VIS_INTERFACE);
LOG.info("ThaumicEnergistics setup complete");
}
public static ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(MODID, path);
}
private void commonSetup(FMLCommonSetupEvent event) {
appeng.blockentity.AEBaseBlockEntity.registerBlockEntityItem(
ModBlockEntities.ESSENTIA_PROVIDER.get(), ModItems.ESSENTIA_PROVIDER_ITEM.get());
appeng.blockentity.AEBaseBlockEntity.registerBlockEntityItem(
ModBlockEntities.INFUSION_PROVIDER.get(), ModItems.INFUSION_PROVIDER_ITEM.get());
event.enqueueWork(() -> {
RecipeRegistration.registerAll();
ThEThaumcraft.init();
GridLinkables.register(ModItems.GOLEM_WIFI_BACKPACK.get(), ItemGolemWirelessBackpack.LINKABLE_HANDLER);
GridLinkables.register(ModItems.WIRELESS_ESSENTIA_TERMINAL.get(),
appeng.items.tools.powered.WirelessTerminalItem.LINKABLE_HANDLER);
});
FeatureRegistry.registerAll();
}
private void registerUpgrades(FMLCommonSetupEvent event) {
event.enqueueWork(() -> {
String essentiaIoBusGroup = GuiText.IOBuses.getTranslationKey();
var importBusItem = ModItems.ESSENTIA_IMPORT_BUS.get();
Upgrades.add(AEItems.FUZZY_CARD, importBusItem, 1, essentiaIoBusGroup);
Upgrades.add(AEItems.REDSTONE_CARD, importBusItem, 1, essentiaIoBusGroup);
Upgrades.add(AEItems.CAPACITY_CARD, importBusItem, 5, essentiaIoBusGroup);
Upgrades.add(AEItems.SPEED_CARD, importBusItem, 4, essentiaIoBusGroup);
Upgrades.add(AEItems.INVERTER_CARD, importBusItem, 1, essentiaIoBusGroup);
var exportBusItem = ModItems.ESSENTIA_EXPORT_BUS.get();
Upgrades.add(AEItems.FUZZY_CARD, exportBusItem, 1, essentiaIoBusGroup);
Upgrades.add(AEItems.REDSTONE_CARD, exportBusItem, 1, essentiaIoBusGroup);
Upgrades.add(AEItems.CAPACITY_CARD, exportBusItem, 5, essentiaIoBusGroup);
Upgrades.add(AEItems.SPEED_CARD, exportBusItem, 4, essentiaIoBusGroup);
Upgrades.add(AEItems.CRAFTING_CARD, exportBusItem, 1, essentiaIoBusGroup);
var storageBusItem = ModItems.ESSENTIA_STORAGE_BUS.get();
Upgrades.add(AEItems.FUZZY_CARD, storageBusItem, 1);
Upgrades.add(AEItems.INVERTER_CARD, storageBusItem, 1);
Upgrades.add(AEItems.CAPACITY_CARD, storageBusItem, 5);
Upgrades.add(AEItems.VOID_CARD, storageBusItem, 1);
});
}
private void registerScreens(RegisterMenuScreensEvent event) {
event.register(ModMenuTypes.ESSENTIA_CELL_WORKBENCH.get(), GuiEssentiaCellWorkbench::new);
event.register(ModMenuTypes.EVC.get(), GuiEssentiaVibrationChamber::new);
event.register(ModMenuTypes.ARCANE_ASSEMBLER.get(), GuiArcaneAssembler::new);
event.register(ModMenuTypes.ESSENTIA_TERMINAL.get(), GuiEssentiaCellTerminal::new);
event.register(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), GuiKnowledgeInscriber::new);
event.register(ModMenuTypes.DISTILLATION_ENCODER.get(), GuiDistillationPatternEncoder::new);
event.register(ModMenuTypes.ESSENTIA_IMPORT_BUS.get(),
(ContainerEssentiaImportBus menu, Inventory inv, Component title)
-> new GuiEssentiaImportBus(menu, inv, title,
StyleManager.loadStyleDoc("/screens/import_bus.json")));
event.register(ModMenuTypes.ESSENTIA_EXPORT_BUS.get(),
(ContainerEssentiaExportBus menu, Inventory inv, Component title)
-> new GuiEssentiaExportBus(menu, inv, title,
StyleManager.loadStyleDoc("/screens/export_bus.json")));
event.register(ModMenuTypes.ESSENTIA_STORAGE_BUS.get(),
(ContainerEssentiaStorageBus menu, Inventory inv, Component title)
-> new GuiEssentiaStorageBus(menu, inv, title,
StyleManager.loadStyleDoc("/screens/storage_bus.json")));
event.register(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get(),
(ContainerArcaneCraftingTerminal menu, Inventory inv, Component title)
-> new GuiArcaneCraftingTerminal(menu, inv, title,
StyleManager.loadStyleDoc("/screens/arcane_crafting_terminal.json")));
event.register(ModMenuTypes.ESSENTIA_LEVEL_EMITTER.get(),
(ContainerEssentiaLevelEmitter menu, Inventory inv, Component title)
-> new GuiEssentiaLevelEmitter(menu, inv, title,
StyleManager.loadStyleDoc("/screens/essentia_level_emitter.json")));
}
private void registerKeyTypes(net.neoforged.neoforge.registries.RegisterEvent event) {
LOG.info("registerKeyTypes fired, registryKey={}", event.getRegistryKey());
if (event.getRegistryKey() == AEKeyType.REGISTRY_KEY) {
AEKeyTypesInternal.register(AEssentiaKeyType.INSTANCE);
LOG.info("AEssentiaKeyType registered successfully!");
}
}
private void registerCapabilities(RegisterCapabilitiesEvent event) {
LOG.info("Registering AE2 grid node capabilities...");
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.ESSENTIA_PROVIDER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.INFUSION_PROVIDER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.ESSENTIA_VIBRATION_CHAMBER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
// 装配器也实现 IInWorldGridNodeHost,必须注册 capability,否则 AE2 线缆连不上 →
// 装配器孤立成自己的网格,ME 终端看不到它提供的可合成配方。
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.ARCANE_ASSEMBLER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
}
}
@@ -0,0 +1,22 @@
package thaumicenergistics.api;
import net.minecraft.resources.ResourceLocation;
import java.util.OptionalLong;
/** 定义 ThaumicEnergistics 可交互的物品与方块实体。自 1.7.10 移植。 */
public interface IThETransportPermissions {
/** 注册一个方块实体类,支持抽取与注入。 */
<T extends net.minecraft.world.level.block.entity.BlockEntity> boolean addTileToBoth(Class<T> tileClass, int capacity);
/** 注册一个仅支持抽取的方块实体。 */
<T extends net.minecraft.world.level.block.entity.BlockEntity> boolean addTileToExtract(Class<T> tileClass, int capacity);
/** 注册一个仅支持注入的方块实体。 */
<T extends net.minecraft.world.level.block.entity.BlockEntity> boolean addTileToInject(Class<T> tileClass, int capacity);
/** 获取方块实体类已注册的容量;未注册返回空。 */
OptionalLong getCapacityForTile(Class<? extends net.minecraft.world.level.block.entity.BlockEntity> tileClass);
/** 检查该方块实体是否允许抽取。 */
boolean canExtract(net.minecraft.world.level.block.entity.BlockEntity tile);
/** 检查该方块实体是否允许注入。 */
boolean canInject(net.minecraft.world.level.block.entity.BlockEntity tile);
/** 注册一个带容量的容器物品类型。 */
void addContainerItem(Class<? extends net.minecraft.world.item.Item> itemClass, int capacity, boolean canHoldPartial);
}
@@ -0,0 +1,6 @@
package thaumicenergistics.api.grid;
/** 合成请求宿主 —— 接收合成请求。移植自 1.7.10。 */
public interface ICraftingIssuerHost {
void launchCrafting(net.minecraft.resources.ResourceLocation aspectId, long amount);
}
@@ -0,0 +1,7 @@
package thaumicenergistics.api.grid;
/** 数字 vis 源——从 AE 网络提供 vis。移植自 1.7.10。 */
public interface IDigiVisSource {
int consumeVis(net.minecraft.resources.ResourceLocation aspectId, int amount);
boolean isActive();
}
@@ -0,0 +1,49 @@
package thaumicenergistics.api.grid;
import net.minecraft.resources.ResourceLocation;
import appeng.api.networking.IGridService;
import thaumicenergistics.api.storage.IAspectStack;
import java.util.List;
/**
* AE2 Grid 网络的源质网格服务。
*
* 通过 {@code grid.getCache(IEssentiaGrid.class)} 获取。
* 提供源质的存取查询接口,供无线傀儡背包、源质提供器等使用。
*
* 注意:这是方案A(轻量级)实现——源质作为独立的 GridService
* 不注册为 AE2 的 AEKeyType。后续可升级为方案B(原生 AEKeyType 集成)。
*/
public interface IEssentiaGrid extends IGridService {
/**
* 获取网络中指定源质的存储量。
*
* @param aspectId 源质 ID(如 "thaumcraft:ignis"
* @return 存储量,不存在返回 0
*/
long getEssentiaAmount(ResourceLocation aspectId);
/**
* 从网络提取源质。
*
* @param aspectId 源质 ID
* @param amount 要提取的量
* @param simulate true 为模拟,false 为实际执行
* @return 实际提取的量
*/
long extractEssentia(ResourceLocation aspectId, long amount, boolean simulate);
/**
* 向网络注入源质。
*
* @param aspectId 源质 ID
* @param amount 要注入的量
* @param simulate true 为模拟,false 为实际执行
* @return 无法注入的量(0 表示全部成功)
*/
long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate);
List<IAspectStack> getEssentiaList();
}
@@ -0,0 +1,8 @@
package thaumicenergistics.api.grid;
import net.minecraft.resources.ResourceLocation;
/** 监听源质网格变化并通知宿主。移植自 1.7.10。 */
public interface IEssentiaWatcher extends java.util.Collection<ResourceLocation> {
IEssentiaWatcherHost getHost();
}
@@ -0,0 +1,9 @@
package thaumicenergistics.api.grid;
import net.minecraft.resources.ResourceLocation;
/** 源质存量变化时通知宿主。移植自 1.7.10。 */
public interface IEssentiaWatcherHost {
void onEssentiaChange(ResourceLocation aspectId, long storedAmount, long changeAmount);
void updateWatcher(IEssentiaWatcher newWatcher);
}
@@ -0,0 +1,8 @@
package thaumicenergistics.api.storage;
import net.minecraft.resources.ResourceLocation;
public record AspectStack(ResourceLocation aspectId, long amount) implements IAspectStack {
public static final AspectStack EMPTY = new AspectStack(ResourceLocation.fromNamespaceAndPath("thaumicenergistics", "empty"), 0);
@Override public boolean isEmpty() { return amount <= 0; }
@Override public IAspectStack copy(long newAmount) { return new AspectStack(aspectId, newAmount); }
}
@@ -0,0 +1,4 @@
package thaumicenergistics.api.storage;
import net.minecraft.resources.ResourceLocation;
public interface IAspectStack { ResourceLocation aspectId(); long amount(); boolean isEmpty(); IAspectStack copy(long newAmount); }
@@ -0,0 +1,21 @@
package thaumicenergistics.client;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
public final class ClientInit {
private ClientInit() {}
public static void onClientSetup(FMLClientSetupEvent event) {
event.enqueueWork(() -> {
appeng.api.client.AEKeyRendering.register(
AEssentiaKeyType.INSTANCE,
AEssentiaKey.class,
new EssentiaKeyRenderHandler()
);
ThaumicEnergistics.LOG.info("EssentiaKeyRenderHandler registered successfully!");
});
}
}
@@ -0,0 +1,82 @@
package thaumicenergistics.client;
import net.minecraft.client.model.geom.EntityModelSet;
import net.minecraft.client.renderer.entity.EntityRenderer;
import net.minecraft.client.renderer.entity.MobRenderer;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.EntityRenderersEvent;
import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent;
import thaumcraft.client.renderers.entity.LegacyGolemModel;
import thaumcraft.client.renderers.entity.ThaumcraftGolemRenderer;
import thaumcraft.common.entities.golem.ThaumcraftGolemEntity;
import thaumcraft.common.registry.TCEntityTypes;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.client.render.layer.GolemBackpackLayer;
import thaumicenergistics.client.render.model.GolemBackpackModel;
import thaumicenergistics.client.render.RenderTileArcaneAssembler;
import thaumicenergistics.client.render.RenderTileEVC;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModItems;
/** 客户端渲染注册的事件处理器。 */
@EventBusSubscriber(modid = ThaumicEnergistics.MODID, value = Dist.CLIENT)
public final class ClientSetup {
/** 背包模型的 LayerDefinition 注册 key */
public static final net.minecraft.client.model.geom.ModelLayerLocation GOLEM_BACKPACK_LAYER =
new net.minecraft.client.model.geom.ModelLayerLocation(
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "golem_backpack"),
"main");
@SubscribeEvent
public static void registerBER(EntityRenderersEvent.RegisterRenderers event) {
event.registerBlockEntityRenderer(ModBlockEntities.ARCANE_ASSEMBLER.get(), RenderTileArcaneAssembler::new);
event.registerBlockEntityRenderer(ModBlockEntities.ESSENTIA_VIBRATION_CHAMBER.get(), RenderTileEVC::new);
}
@SubscribeEvent
public static void registerLayerDefinitions(EntityRenderersEvent.RegisterLayerDefinitions event) {
event.registerLayerDefinition(GOLEM_BACKPACK_LAYER, GolemBackpackModel::createBodyLayer);
}
@SubscribeEvent
public static void addEntityLayers(EntityRenderersEvent.AddLayers event) {
// 给 Thaumcraft 的傀儡渲染器添加背包渲染层
EntityRenderer<?> renderer = event.getRenderer(TCEntityTypes.GOLEM.get());
if (renderer instanceof ThaumcraftGolemRenderer golemRenderer) {
EntityModelSet modelSet = event.getEntityModels();
net.minecraft.client.model.geom.ModelPart backpackPart = modelSet.bakeLayer(GOLEM_BACKPACK_LAYER);
GolemBackpackModel backpackModel = new GolemBackpackModel(backpackPart);
golemRenderer.addLayer(new GolemBackpackLayer(golemRenderer, backpackModel));
}
}
@SubscribeEvent
public static void registerItemColors(RegisterColorHandlersEvent.Item event) {
// 给源质终端物品着色:完全对齐 AEColor.TRANSPARENT.getVariantByTintIndex()
event.register((stack, tintIndex) -> {
return switch (tintIndex) {
case 0 -> -1; // 不染色
case 1 -> 0xFF5a479e; // Dark: 深紫色
case 2 -> 0xFF915dcd; // Medium: 紫色
case 3 -> 0xFFe2a3e3; // Bright: 淡紫色
case 4 -> 0xFFb980d8; // Medium Bright: 中间紫
default -> -1;
};
}, ModItems.ESSENTIA_TERMINAL_ITEM.get());
// 给无线源质终端物品着色(与源质终端部件相同的紫色)
event.register((stack, tintIndex) -> {
return switch (tintIndex) {
case 0 -> -1;
case 1 -> 0xFF5a479e;
case 2 -> 0xFF915dcd;
case 3 -> 0xFFe2a3e3;
case 4 -> 0xFFb980d8;
default -> -1;
};
}, ModItems.WIRELESS_ESSENTIA_TERMINAL.get());
}
}
@@ -0,0 +1,61 @@
package thaumicenergistics.client;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import java.util.ArrayList;
import java.util.List;
public class EssentiaKeyRenderHandler implements appeng.api.client.AEKeyRenderHandler<AEssentiaKey> {
@Override
public void drawInGui(Minecraft minecraft, GuiGraphics guiGraphics, int x, int y, AEssentiaKey stack) {
String tag = stack.getId().getPath();
// 渲染要素图标(透明背景):TC 的 aspect 图标是白色剪影,需用 aspect 颜色着色(与 JEI 的 AspectGuiRenderer 一致)
int color = getAspectColor(tag);
float r = ((color >> 16) & 0xFF) / 255.0f;
float g = ((color >> 8) & 0xFF) / 255.0f;
float b = (color & 0xFF) / 255.0f;
guiGraphics.setColor(r, g, b, 1.0f);
guiGraphics.blit(aspectTexture(tag), x, y, 0, 0, 16, 16, 16, 16);
guiGraphics.setColor(1.0f, 1.0f, 1.0f, 1.0f);
}
/** 按 tag 缓存 aspect 贴图 RL,避免每帧 fromNamespaceAndPath。 */
private static final java.util.Map<String, ResourceLocation> TEXTURE_CACHE = new java.util.HashMap<>();
private static ResourceLocation aspectTexture(String tag) {
return TEXTURE_CACHE.computeIfAbsent(tag,
t -> ResourceLocation.fromNamespaceAndPath("thaumcraft", "textures/aspects/" + t + ".png"));
}
/** 获取 aspect 的 ARGB 颜色(按 tag 查 Thaumcraft Aspect),未知返回白色。 */
private static int getAspectColor(String tag) {
thaumcraft.api.aspects.Aspect aspect = thaumcraft.api.aspects.Aspect.getAspect(tag);
return aspect != null ? aspect.getColor() : 0xFFFFFFFF;
}
@Override
public void drawOnBlockFace(PoseStack poseStack, MultiBufferSource buffers, AEssentiaKey what,
float scale, int combinedLight, Level level) {
}
@Override
public Component getDisplayName(AEssentiaKey stack) {
return stack.getDisplayName(); // 由 AEssentiaKey.computeDisplayName() 返回源质真实名称
}
@Override
public List<Component> getTooltip(AEssentiaKey stack) {
List<Component> tooltip = new ArrayList<>();
tooltip.add(stack.getDisplayName());
tooltip.add(Component.literal(appeng.util.Platform.formatModName(stack.getModId())));
return tooltip;
}
}
@@ -0,0 +1,19 @@
package thaumicenergistics.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/** 奥术装配器的渲染器——显示合成进度动画。 */
public class RenderTileArcaneAssembler implements BlockEntityRenderer<TileArcaneAssembler> {
public RenderTileArcaneAssembler(BlockEntityRendererProvider.Context ctx) {}
@Override
public void render(TileArcaneAssembler tile, float partialTick, PoseStack poseStack,
MultiBufferSource buffer, int packedLight, int packedOverlay) {
// TODO: Render crafting progress bar and speed upgrade indicators
}
}
@@ -0,0 +1,19 @@
package thaumicenergistics.client.render;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.blockentity.BlockEntityRenderer;
import net.minecraft.client.renderer.blockentity.BlockEntityRendererProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
/** 源质谐振仓的渲染器——显示燃烧进度。 */
public class RenderTileEVC implements BlockEntityRenderer<TileEssentiaVibrationChamber> {
public RenderTileEVC(BlockEntityRendererProvider.Context ctx) {}
@Override
public void render(TileEssentiaVibrationChamber tile, float partialTick, PoseStack poseStack,
MultiBufferSource buffer, int packedLight, int packedOverlay) {
// TODO: Render flame/burn animation proportional to burnProgress
}
}
@@ -0,0 +1,90 @@
package thaumicenergistics.client.render.layer;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.RenderLayerParent;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.client.renderer.texture.OverlayTexture;
import net.minecraft.resources.ResourceLocation;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.api.distmarker.OnlyIn;
import thaumcraft.client.renderers.entity.LegacyGolemModel;
import thaumcraft.common.entities.golem.ThaumcraftGolemEntity;
import thaumicenergistics.client.render.model.GolemBackpackModel;
import thaumicenergistics.common.entities.BackpackSkins;
import thaumicenergistics.common.network.GolemBackpackClientData;
import thaumicenergistics.common.network.GolemBackpackSyncPacket;
/**
* 傀儡无线背包渲染层。
* 当傀儡装备了无线背包时,在背上渲染天线 + 背包盒(使用当前皮肤纹理),以及旋转的福鲁伊克斯珍珠(绿色=连接,红色=断开)。
* 数据来源:{@link GolemBackpackClientData}(由 S2C 网络包更新)。
* 皮肤和连接状态均从客户端缓存读取,不直接查询 NBT。
*/
@OnlyIn(Dist.CLIENT)
public class GolemBackpackLayer extends RenderLayer<ThaumcraftGolemEntity, LegacyGolemModel> {
/** 默认纹理(Thaumium 皮肤) */
private static final ResourceLocation DEFAULT_TEXTURE =
BackpackSkins.Thaumium.getTextureLocation();
/** 珍珠旋转速度(度/tick */
private static final float PEARL_ROTATION_SPEED = 2.0f;
private final GolemBackpackModel model;
public GolemBackpackLayer(RenderLayerParent<ThaumcraftGolemEntity, LegacyGolemModel> parent,
GolemBackpackModel model) {
super(parent);
this.model = model;
}
@Override
public void render(PoseStack poseStack, MultiBufferSource bufferSource, int packedLight,
ThaumcraftGolemEntity golem, float limbSwing, float limbSwingAmount,
float partialTick, float ageInTicks, float netHeadYaw, float headPitch) {
// 仅在傀儡装备了背包时渲染(从客户端缓存读取)
if (!GolemBackpackClientData.hasBackpack(golem)) {
return;
}
// 从客户端缓存读取连接状态和皮肤
boolean inRange = GolemBackpackClientData.isInRange(golem);
BackpackSkins skin = GolemBackpackClientData.getSkin(golem);
ResourceLocation texture = skin.getTextureLocation();
float pearlRotation = (golem.tickCount + partialTick) * PEARL_ROTATION_SPEED;
VertexConsumer buffer = bufferSource.getBuffer(cutoutRenderType(texture));
// 对齐到傀儡身体
poseStack.pushPose();
// 傀儡模型尺寸约 0.4×0.95,背包放在背部偏上
poseStack.scale(0.5f, 0.5f, 0.5f);
// 略微后移到背上
poseStack.translate(0, 0.5, -1.5);
model.renderToBuffer(poseStack, buffer, packedLight, OverlayTexture.NO_OVERLAY, pearlRotation);
// 渲染珍珠(使用 entityTranslucent 允许半透明效果)
VertexConsumer pearlBuffer = bufferSource.getBuffer(translucentRenderType(texture));
model.renderPearl(poseStack, pearlBuffer, packedLight, OverlayTexture.NO_OVERLAY,
pearlRotation, inRange);
poseStack.popPose();
}
/** 按贴图缓存 RenderType,避免每帧创建(渲染主线程单线程安全)。 */
private static final java.util.Map<ResourceLocation, RenderType> CUTOUT_CACHE = new java.util.HashMap<>();
private static final java.util.Map<ResourceLocation, RenderType> TRANSLUCENT_CACHE = new java.util.HashMap<>();
private static RenderType cutoutRenderType(ResourceLocation tex) {
return CUTOUT_CACHE.computeIfAbsent(tex, RenderType::entityCutoutNoCull);
}
private static RenderType translucentRenderType(ResourceLocation tex) {
return TRANSLUCENT_CACHE.computeIfAbsent(tex, RenderType::entityTranslucent);
}
}
@@ -0,0 +1,181 @@
package thaumicenergistics.client.render.model;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.PartPose;
import net.minecraft.client.model.geom.builders.CubeListBuilder;
import net.minecraft.client.model.geom.builders.LayerDefinition;
import net.minecraft.client.model.geom.builders.MeshDefinition;
import net.minecraft.client.model.geom.builders.PartDefinition;
import org.joml.Matrix4f;
import org.joml.Vector4f;
/**
* 傀儡无线背包模型(简化版)。
*
* 包含三个部件:Antenna(天线,1×3×1)、PackBack(背包主体,2×6×6)、PackFront(背包前面板,1×2×4)。
* 旋转的福鲁伊克斯珍珠由 GolemBackpackLayer 在渲染时手动绘制。
* 对应 1.7.10 的 ModelGolemWifiBackpack,简化为统一的 Thaumium 皮肤。
*/
public class GolemBackpackModel {
private final ModelPart root;
public GolemBackpackModel(ModelPart root) {
this.root = root;
}
/**
* 创建 LayerDefinition,供 {@code EntityRenderersEvent.RegisterLayerDefinitions} 注册。
* 纹理尺寸 16×16,与 1.7.10 一致。
*/
public static LayerDefinition createBodyLayer() {
MeshDefinition mesh = new MeshDefinition();
PartDefinition part = mesh.getRoot();
// 天线:1×3×1,纹理偏移 (10,0),X 轴旋转 180°
part.addOrReplaceChild("antenna",
CubeListBuilder.create().texOffs(10, 0)
.addBox(-0.5f, -6f, -0.5f, 1, 3, 1),
PartPose.rotation((float) Math.PI, 0, 0));
// 背包主体:2×6×6,纹理偏移 (0,0),X 轴旋转 180°
part.addOrReplaceChild("pack_back",
CubeListBuilder.create().texOffs(0, 0)
.addBox(-1f, -3f, -3f, 2, 6, 6),
PartPose.rotation((float) Math.PI, 0, 0));
// 背包前面板:1×2×4,纹理偏移 (2,0),X 轴旋转 180°
part.addOrReplaceChild("pack_front",
CubeListBuilder.create().texOffs(2, 0)
.addBox(-1.5f, -1f, -2f, 1, 2, 4),
PartPose.rotation((float) Math.PI, 0, 0));
return LayerDefinition.create(mesh, 16, 16);
}
/**
* 渲染背包箱体(天线 + 主体 + 前面板)。
*/
public void renderToBuffer(PoseStack poseStack, VertexConsumer buffer,
int light, int overlay, float pearlRotation) {
root.render(poseStack, buffer, light, overlay);
}
/**
* 渲染旋转的福鲁伊克斯珍珠:位于天线上方,4 个面围绕天线绘制,随 pearlRotation 旋转。
* UV 坐标:minU=0.5, maxU=0.84375, minV=0.375, maxV=0.7187516×16 纹理)。
*
* @param poseStack PoseStack
* @param buffer VertexConsumer
* @param light packed light
* @param overlay packed overlay
* @param pearlRotation 旋转角度(度)
* @param inRange true=正常紫色,false=红色(不在范围内)
*/
public void renderPearl(PoseStack poseStack, VertexConsumer buffer,
int light, int overlay, float pearlRotation, boolean inRange) {
float pearlScale = 0.2f;
float antennaWidth = 0.32f;
float faceDist = -0.013f / pearlScale;
float twoFD = faceDist + faceDist;
// UV 坐标(16×16 纹理)
float minU = 8.0f / 16.0f;
float maxU = 13.5f / 16.0f;
float minV = 6.0f / 16.0f;
float maxV = 11.5f / 16.0f;
// 颜色:不在范围内时红色
int red = inRange ? 255 : 255;
int green = inRange ? 255 : 0;
int blue = inRange ? 255 : 0;
poseStack.pushPose();
poseStack.mulPose(com.mojang.math.Axis.YP.rotationDegrees(pearlRotation));
poseStack.scale(pearlScale, pearlScale, pearlScale);
// 左面
poseStack.pushPose();
poseStack.translate(-0.0955f, 0.275f, -0.045f);
drawPearlFace(poseStack, buffer, light, overlay, minU, maxU, minV, maxV, red, green, blue, 0, 0, 1);
// 右面
poseStack.translate(0, 0, antennaWidth - twoFD);
drawPearlFace(poseStack, buffer, light, overlay, maxU, minU, minV, maxV, red, green, blue, 0, 0, -1);
poseStack.popPose();
// 前后两面(旋转 90°)
poseStack.pushPose();
poseStack.mulPose(com.mojang.math.Axis.YP.rotationDegrees(90));
poseStack.translate(-0.0955f, 0.275f, -0.045f - antennaWidth - faceDist);
drawPearlFace(poseStack, buffer, light, overlay, maxU, minU, minV, maxV, red, green, blue, 0, 0, -1);
poseStack.translate(0, 0, antennaWidth - twoFD);
drawPearlFace(poseStack, buffer, light, overlay, minU, maxU, minV, maxV, red, green, blue, 0, 0, 1);
poseStack.popPose();
poseStack.popPose();
}
/**
* 绘制珍珠的一个面(正反两面,共 8 个顶点),手动通过矩阵变换顶点坐标。
* 使用 1.21.1 的 VertexConsumer 新 APIaddVertex + setColor + setUv + setOverlay + setLight + setNormal。
*/
private void drawPearlFace(PoseStack poseStack, VertexConsumer buffer, int light, int overlay,
float minU, float maxU, float minV, float maxV,
int r, int g, int b, float nx, float ny, float nz) {
Matrix4f matrix = poseStack.last().pose();
// 四个角点的本地坐标
float[][] corners = {
{0, 0, 0}, // 左下
{1, 0, 0}, // 右下
{1, 1, 0}, // 右上
{0, 1, 0} // 左上
};
// 正面 UV
float[][] frontUvs = {
{maxU, maxV},
{minU, maxV},
{minU, minV},
{maxU, minV}
};
// 背面 UV(镜像)
float[][] backUvs = {
{maxU, minV},
{minU, minV},
{minU, maxV},
{maxU, maxV}
};
Vector4f transformed = new Vector4f();
// 正面
for (int i = 0; i < 4; i++) {
transformed.set(corners[i][0], corners[i][1], corners[i][2], 1.0f);
matrix.transform(transformed);
buffer.addVertex(transformed.x, transformed.y, transformed.z)
.setColor(r, g, b, 255)
.setUv(frontUvs[i][0], frontUvs[i][1])
.setOverlay(overlay)
.setLight(light)
.setNormal(nx, ny, nz);
}
// 背面(逆序 + 翻转法线)
for (int i = 0; i < 4; i++) {
transformed.set(corners[i][0], corners[i][1], corners[i][2], 1.0f);
matrix.transform(transformed);
buffer.addVertex(transformed.x, transformed.y, transformed.z)
.setColor(r, g, b, 255)
.setUv(backUvs[i][0], backUvs[i][1])
.setOverlay(overlay)
.setLight(light)
.setNormal(-nx, -ny, -nz);
}
}
}
@@ -0,0 +1,56 @@
package thaumicenergistics.common;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Rotation;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import thaumcraft.common.items.TCFunctionalItems;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.items.ItemFocusAEWrench;
import thaumicenergistics.init.ModItems;
@EventBusSubscriber(modid = ThaumicEnergistics.MODID)
public final class WrenchFocusHandler {
private WrenchFocusHandler() {
}
@SubscribeEvent
public static void onLeftClickBlock(PlayerInteractEvent.LeftClickBlock event) {
Player player = event.getEntity();
Level level = player.level();
ItemStack heldStack = player.getMainHandItem();
if (!(heldStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return;
}
ItemStack focusStack = wand.getFocus(heldStack);
if (focusStack.isEmpty() || focusStack.getItem() != ModItems.FOCUS_AEWRENCH.get()) {
return;
}
if (!player.isShiftKeyDown()) {
return;
}
if (level.isClientSide()) {
return;
}
BlockPos pos = event.getPos();
BlockState state = level.getBlockState(pos);
BlockState rotated = state.rotate(level, pos, Rotation.CLOCKWISE_90);
if (rotated != state) {
level.setBlock(pos, rotated, 3);
ItemFocusAEWrench.spawnBeamParticles(level, player, pos);
event.setCanceled(true);
}
}
}
@@ -0,0 +1,71 @@
package thaumicenergistics.common.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.ItemInteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.block.state.properties.DirectionProperty;
import net.minecraft.world.phys.BlockHitResult;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumicenergistics.common.tiles.ThETileBase;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
public abstract class OrientableEntityBlock extends Block implements EntityBlock {
public static final DirectionProperty FACING = DirectionProperty.create("facing", Direction.Plane.HORIZONTAL);
protected OrientableEntityBlock(Properties props) {
super(props);
this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));
}
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING);
}
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}
@Override
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (level.isClientSide()) return ItemInteractionResult.SUCCESS;
BlockEntity be = level.getBlockEntity(pos);
if (!(be instanceof TileEssentiaVibrationChamber chamber)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
// 检查手持物品是否为源质安瓿瓶
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
AspectList aspects = container.getAspects(stack);
if (aspects == null || aspects.size() == 0) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
Aspect aspect = aspects.getAspects()[0];
if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) {
chamber.setSuctionType(aspect);
return ItemInteractionResult.SUCCESS;
}
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
if (!state.is(newState.getBlock())) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof ThETileBase tb) tb.onBreakBlock();
}
super.onRemove(state, level, pos, newState, moved);
}
}
@@ -0,0 +1,47 @@
package thaumicenergistics.common.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.Level;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.common.tiles.TileInfusionProvider;
import thaumicenergistics.common.tiles.ThETileBase;
public abstract class ThEBaseEntityBlock extends Block implements EntityBlock {
protected ThEBaseEntityBlock(Properties p) { super(p); }
@Override
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
if (level.isClientSide()) return null;
if (type == thaumicenergistics.init.ModBlockEntities.ESSENTIA_PROVIDER.get()) {
return createTicker((lvl, pos, st, be) -> ((TileEssentiaProvider) be).serverTick());
}
if (type == thaumicenergistics.init.ModBlockEntities.INFUSION_PROVIDER.get()) {
return createTicker((lvl, pos, st, be) -> ((TileInfusionProvider) be).serverTick());
}
return null;
}
@SuppressWarnings("unchecked")
private static <T extends BlockEntity> BlockEntityTicker<T> createTicker(BlockEntityTicker<? super T> ticker) {
return (BlockEntityTicker<T>) ticker;
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
if (!state.is(newState.getBlock())) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof ThETileBase tb) tb.onBreakBlock();
}
super.onRemove(state, level, pos, newState, moved);
}
}
@@ -0,0 +1,260 @@
package thaumicenergistics.common.container;
import appeng.core.definitions.AEItems;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.IVisDiscountGear;
import thaumcraft.api.IWarpingGear;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.init.ModMenuTypes;
/**
* 奥术装配器 Container。
*
* 坐标完全对应1.7.10 ThaumicEnergistics
* 玩家背包 Y=115,快捷栏 Y=173
* 3×7 配方槽 (26, 25)
* 知识核心 (187, 8)
* 4加速卡 (187, 26)
* 目标产物 (14, 87)
* 4折扣护甲槽 (210, 26)
*
* 不继承 ThEContainerBase,因为基类把玩家背包放在AE2标准 Y=169,
* 而奥术装配器GUI贴图的背包在更高位置 Y=115。
*/
public class ContainerArcaneAssembler extends AbstractContainerMenu {
// ===== 坐标常量(1:1 对应1.7.10 =====
private static final int PLAYER_INV_X = 8;
private static final int PLAYER_INV_Y = 115;
private static final int HOTBAR_Y = PLAYER_INV_Y + 58; // 173
private static final int PATTERN_X = 26;
private static final int PATTERN_Y = 25;
private static final int PATTERN_ROWS = 3;
private static final int PATTERN_COLS = 7;
private static final int KCORE_X = 187;
private static final int KCORE_Y = 8;
private static final int UPGRADE_X = 187;
private static final int UPGRADE_Y = 26;
private static final int UPGRADE_COUNT = 4;
private static final int TARGET_X = 14;
private static final int TARGET_Y = 87;
private static final int ARMOR_X = 210;
private static final int ARMOR_Y = 26;
private static final int ARMOR_COUNT = 4;
// ===== 槽位划分(quickMoveStack 用) =====
// 顺序 = 玩家(36) → KCore(1) → 3×7配方(21) → 升级卡(4) → 目标(1) → 护甲(4) = 67
private static final int PLAYER_END = 36;
private static final int KCORE_IDX = 36;
private static final int PATTERN_START = 37;
private static final int PATTERN_END = PATTERN_START + PATTERN_ROWS * PATTERN_COLS; // 58
private static final int UPGRADE_START = PATTERN_END; // 58
private static final int UPGRADE_END = UPGRADE_START + UPGRADE_COUNT; // 62
private static final int TARGET_IDX = UPGRADE_END; // 62
private static final int ARMOR_START = TARGET_IDX + 1; // 63
private static final int ARMOR_END = ARMOR_START + ARMOR_COUNT; // 67
/** 升级卡 handler(独立4格,与 Tile 的 speedUpgrades 字段同步) */
final ItemStackHandler upgradeHandler = new ItemStackHandler(UPGRADE_COUNT) {
@Override protected void onContentsChanged(int slot) {
super.onContentsChanged(slot);
if (assembler != null) {
int count = 0;
for (int i = 0; i < UPGRADE_COUNT; i++)
if (!getStackInSlot(i).isEmpty()) count++;
assembler.setSpeedUpgrades(count);
}
}
};
public final TileArcaneAssembler assembler;
/** 服务端构造(由 Block#getMenuProvider 调用,不带 tile */
public ContainerArcaneAssembler(int id, Inventory inv) {
this(id, inv, (TileArcaneAssembler) null);
}
/** 服务端构造(带 tile 引用) */
public ContainerArcaneAssembler(int id, Inventory inv, TileArcaneAssembler tile) {
super(ModMenuTypes.ARCANE_ASSEMBLER.get(), id);
this.assembler = tile;
// ===== 1. 玩家背包 3行 =====
for (int r = 0; r < 3; r++)
for (int c = 0; c < 9; c++)
addSlot(new Slot(inv, c + r * 9 + 9,
PLAYER_INV_X + c * 18, PLAYER_INV_Y + r * 18));
// ===== 2. 快捷栏 1行 =====
for (int c = 0; c < 9; c++)
addSlot(new Slot(inv, c, PLAYER_INV_X + c * 18, HOTBAR_Y));
ItemStackHandler machineHandler = wrapMachineInventory();
// ===== 3. 知识核心 (Tile.KCORE_SLOT = 0) =====
addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.KCORE_SLOT, KCORE_X, KCORE_Y) {
@Override public boolean mayPlace(ItemStack stack) {
return stack.is(thaumicenergistics.init.ModItems.KNOWLEDGE_CORE.get());
}
});
// ===== 4. 3×7 配方槽(只读展示) =====
for (int r = 0; r < PATTERN_ROWS; r++)
for (int c = 0; c < PATTERN_COLS; c++) {
int invIdx = TileArcaneAssembler.PATTERN_SLOT_START + r * PATTERN_COLS + c;
addSlot(new SlotItemHandler(machineHandler, invIdx,
PATTERN_X + c * 18, PATTERN_Y + r * 18) {
@Override public boolean mayPlace(ItemStack s) { return false; }
@Override public boolean mayPickup(Player p) { return false; }
@Override public int getMaxStackSize() { return 1; }
});
}
// ===== 5. 4张AE2速度卡 =====
for (int i = 0; i < UPGRADE_COUNT; i++) {
addSlot(new SlotItemHandler(upgradeHandler, i, UPGRADE_X, UPGRADE_Y + i * 18) {
@Override
public boolean mayPlace(ItemStack s) {
return AEItems.SPEED_CARD.is(s);
}
});
}
// ===== 6. 目标产物输出槽(只读展示) =====
addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y) {
@Override public boolean mayPlace(ItemStack s) { return false; }
@Override public boolean mayPickup(Player p) { return false; }
});
// ===== 7. 4个护甲折扣槽 =====
for (int i = 0; i < ARMOR_COUNT; i++) {
final int armorIdx = i;
int invIdx = TileArcaneAssembler.ARMOR_SLOT_START + i;
final net.minecraft.world.entity.EquipmentSlot eq = switch (i) {
case 0 -> net.minecraft.world.entity.EquipmentSlot.HEAD;
case 1 -> net.minecraft.world.entity.EquipmentSlot.CHEST;
case 2 -> net.minecraft.world.entity.EquipmentSlot.LEGS;
default -> net.minecraft.world.entity.EquipmentSlot.FEET;
};
addSlot(new SlotItemHandler(machineHandler, invIdx, ARMOR_X, ARMOR_Y + i * 18) {
@Override public boolean mayPlace(ItemStack s) {
if (s.getItem() instanceof IVisDiscountGear) return true;
if (s.getItem() instanceof IWarpingGear) return true;
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem
&& eqItem.getEquipmentSlot() == eq) return true;
return s.canEquip(eq, null);
}
@Override public void setChanged() {
super.setChanged();
if (assembler != null)
assembler.onInventoryChanged(TileArcaneAssembler.ARMOR_SLOT_START + armorIdx);
}
});
}
}
/** 客户端构造(IMenuTypeExtension 反射调用) */
public ContainerArcaneAssembler(int id, Inventory inv, RegistryFriendlyByteBuf buf) {
this(id, inv);
}
private ItemStackHandler wrapMachineInventory() {
final net.minecraft.world.SimpleContainer inv =
(assembler != null) ? assembler.getInternalInventory()
: new net.minecraft.world.SimpleContainer(TileArcaneAssembler.SLOT_COUNT);
return new ItemStackHandler(inv.getContainerSize()) {
@Override public int getSlots() { return inv.getContainerSize(); }
@Override public ItemStack getStackInSlot(int slot) { return inv.getItem(slot); }
@Override public void setStackInSlot(int slot, ItemStack stack) { inv.setItem(slot, stack); }
@Override public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) {
if (stack.isEmpty()) return ItemStack.EMPTY;
ItemStack cur = inv.getItem(slot);
if (!cur.isEmpty()) return stack;
if (!simulate) {
ItemStack cp = stack.copy(); cp.setCount(1);
inv.setItem(slot, cp);
}
ItemStack leftover = stack.copy();
leftover.shrink(1);
return leftover;
}
@Override public ItemStack extractItem(int slot, int amount, boolean simulate) {
ItemStack cur = inv.getItem(slot);
if (cur.isEmpty()) return ItemStack.EMPTY;
int take = Math.min(amount, cur.getCount());
ItemStack res = cur.copy(); res.setCount(take);
if (!simulate) {
ItemStack left = cur.copy(); left.shrink(take);
inv.setItem(slot, left.isEmpty() ? ItemStack.EMPTY : left);
}
return res;
}
@Override public int getSlotLimit(int slot) { return 1; }
};
}
// ===== Shift+点击 =====
@Override public ItemStack quickMoveStack(Player player, int index) {
ItemStack result = ItemStack.EMPTY;
Slot slot = slots.get(index);
if (slot == null || !slot.hasItem()) return result;
ItemStack stack = slot.getItem();
result = stack.copy();
if (index < PLAYER_END) {
// 玩家背包 → 知识核心 → 护甲 → 速度卡 → 背包↔快捷栏
boolean merged = false;
if (stack.is(thaumicenergistics.init.ModItems.KNOWLEDGE_CORE.get())) {
merged = moveItemStackTo(stack, KCORE_IDX, KCORE_IDX + 1, false);
}
if (!merged && (stack.getItem() instanceof IVisDiscountGear
|| stack.getItem() instanceof IWarpingGear
|| stack.getItem() instanceof net.minecraft.world.item.ArmorItem
|| stack.getItem() instanceof net.minecraft.world.item.Equipable)) {
for (int a = 0; a < ARMOR_COUNT && !merged; a++) {
Slot armorSlot = slots.get(ARMOR_START + a);
if (armorSlot.mayPlace(stack) && !armorSlot.hasItem()) {
ItemStack one = stack.copy(); one.setCount(1);
armorSlot.set(one);
stack.shrink(1);
merged = true;
}
}
}
if (!merged && AEItems.SPEED_CARD.is(stack)) {
merged = moveItemStackTo(stack, UPGRADE_START, UPGRADE_END, false);
}
if (!merged) {
if (index < 27) merged = moveItemStackTo(stack, 27, PLAYER_END, false);
else merged = moveItemStackTo(stack, 0, 27, false);
}
if (!merged) return ItemStack.EMPTY;
} else {
// 机器槽 → 玩家背包
if (!moveItemStackTo(stack, 0, PLAYER_END, true)) return ItemStack.EMPTY;
}
if (stack.isEmpty()) slot.set(ItemStack.EMPTY);
else slot.setChanged();
return result;
}
@Override public boolean stillValid(Player player) { return true; }
}
@@ -0,0 +1,393 @@
package thaumicenergistics.common.container;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingInput;
import net.minecraft.world.item.crafting.RecipeType;
import appeng.api.inventories.ISegmentedInventory;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.stacks.AEItemKey;
import appeng.api.storage.ITerminalHost;
import appeng.helpers.ICraftingGridMenu;
import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync;
import appeng.menu.me.common.MEStorageMenu;
import appeng.menu.me.crafting.CraftConfirmMenu;
import appeng.menu.slot.AppEngSlot;
import appeng.menu.slot.CraftingMatrixSlot;
import appeng.me.storage.LinkStatusRespectingInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import net.minecraft.world.item.crafting.Ingredient;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
import thaumicenergistics.init.ModMenuTypes;
public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements ICraftingGridMenu {
@GuiSync(20)
public String requiredAspectsData = "";
@GuiSync(21)
public boolean hasValidRecipe = false;
/**
* 防止 onTake 期间 slotsChanged 的中间态干扰配方检测。
* 在 ArcaneCraftingResultSlot.onTake 开始前设为 true,完成后设为 false。
*/
private boolean isCraftingResultBeingTaken = false;
private final ISegmentedInventory craftingInventoryHost;
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
private final ArcaneCraftingResultSlot resultSlot;
public ContainerArcaneCraftingTerminal(int id, Inventory ip, ITerminalHost host) {
this(ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get(), id, ip, host, true);
}
public ContainerArcaneCraftingTerminal(MenuType<?> menuType, int id, Inventory ip, ITerminalHost host,
boolean bindInventory) {
super(menuType, id, ip, host, bindInventory);
this.craftingInventoryHost = (ISegmentedInventory) host;
var craftingGridInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
for (int i = 0; i < 9; i++) {
this.craftingSlots[i] = new CraftingMatrixSlot(this, craftingGridInv, i);
this.addSlot(this.craftingSlots[i], SlotSemantics.CRAFTING_GRID);
}
var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus);
this.resultSlot = new ArcaneCraftingResultSlot(getPlayerInventory().player, craftingGridInv);
this.resultSlot.setRefreshCallback(() -> updateCurrentRecipeAndOutput(false));
this.resultSlot.setContainer(this);
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
this.addSlot(new AppEngSlot(wandInv, ArcaneCraftingTerminalPart.WAND_SLOT_INDEX) {
@Override
public int getMaxStackSize() {
return 1;
}
@Override
public void setChanged() {
super.setChanged();
// 法杖槽变化时立即重检配方,更新 resultSlot 的 wandStack
updateCurrentRecipeAndOutput(false);
}
}, SlotSemantics.STORAGE);
updateCurrentRecipeAndOutput(true);
}
// ========== ICraftingGridMenu 实现 ==========
@Override
public InternalInventory getCraftingMatrix() {
var inv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
ThaumicEnergistics.LOG.debug("[ICraftingGrid] getCraftingMatrix called, size={}", inv.size());
return inv;
}
@Override
public void startAutoCrafting(List<AutoCraftEntry> toCraft) {
CraftConfirmMenu.openWithCraftingList(getActionHost(), (ServerPlayer) getPlayer(), getLocator(), toCraft);
}
@Override
public IEnergySource getEnergySource() {
return this.energySource;
}
// ========== 配方检测 ==========
@Override
public void slotsChanged(Container inventory) {
ThaumicEnergistics.LOG.debug("[A] slotsChanged fired, beingTaken={}, inventoryClass={}",
isCraftingResultBeingTaken, inventory.getClass().getSimpleName());
if (!isCraftingResultBeingTaken) {
updateCurrentRecipeAndOutput(false);
} else {
ThaumicEnergistics.LOG.debug("[A] slotsChanged SKIPPED (beingTaken=true)");
}
}
public void setCraftingResultBeingTaken(boolean beingTaken) {
ThaumicEnergistics.LOG.debug("[A] setCraftingResultBeingTaken={}", beingTaken);
this.isCraftingResultBeingTaken = beingTaken;
}
private static final int CENTIVIS = 100;
private boolean hasEnoughVis(AspectList cost) {
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
if (cost == null || cost.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] no cost, no wand needed");
return true;
}
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0);
ThaumicEnergistics.LOG.debug("[V] wandStack={}", wandStack);
if (wandStack.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] wandStack EMPTY");
return false;
}
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
ThaumicEnergistics.LOG.debug("[V] not a WandCastingItem");
return false;
}
var centivisCost = new AspectList();
for (var aspect : cost.aspects()) {
centivisCost.add(aspect, cost.amount(aspect) * CENTIVIS);
}
boolean result = wand.consumeVisCost(wandStack, getPlayerInventory().player, centivisCost, false, true);
ThaumicEnergistics.LOG.debug("[V] consumeVisCost result={}", result);
return result;
}
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate);
var player = getPlayerInventory().player;
var level = player.level();
boolean foundArcane = false;
AspectList requiredAspects = null;
if (level.getServer() != null) {
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] findMatchingArcaneRecipe result={}", arcaneResult);
if (!arcaneResult.isEmpty()) {
requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
ThaumicEnergistics.LOG.debug("[R] ARCANE MATCH! aspects={}, result={}", requiredAspects, arcaneResult);
foundArcane = true;
if (hasEnoughVis(requiredAspects)) {
ThaumicEnergistics.LOG.debug("[R] Vis OK → showing result");
resultSlot.setDisplayedCraftingOutput(arcaneResult);
} else {
ThaumicEnergistics.LOG.debug("[R] Vis INSUFFICIENT → result empty, but recipe recognized");
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
}
} else {
ThaumicEnergistics.LOG.debug("[R] No arcane recipe match");
}
}
if (!foundArcane) {
ThaumicEnergistics.LOG.debug("[R] No arcane match, trying vanilla...");
var testItems = new ArrayList<ItemStack>(this.craftingSlots.length);
for (var craftingSlot : this.craftingSlots) {
testItems.add(craftingSlot.getItem().copy());
}
var testInput = CraftingInput.of(3, 3, testItems);
var recipeHolder = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, testInput, level);
if (recipeHolder.isPresent()) {
ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess());
ThaumicEnergistics.LOG.debug("[R] Vanilla match: {}, setting result slot", result);
resultSlot.setDisplayedCraftingOutput(result);
} else {
ThaumicEnergistics.LOG.debug("[R] No vanilla match either, clearing result slot");
// 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态
if (!level.isClientSide()) {
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
}
}
}
if (foundArcane) {
// 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值
ThaumicEnergistics.LOG.debug("[R] foundArcane branch: isClientSide={}", level.isClientSide());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: keeping server-synced values, not overriding");
return;
}
this.hasValidRecipe = true;
resultSlot.setRequiredAspects(requiredAspects);
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var ws = wandInv.getStackInSlot(0);
resultSlot.setWandStack(ws);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=true, wandStack={}", ws);
if (requiredAspects != null && requiredAspects.size() > 0) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<Aspect, Integer> entry : requiredAspects.aspects.entrySet()) {
if (!sb.isEmpty()) sb.append(",");
sb.append(entry.getKey().getTag()).append("=").append(entry.getValue());
}
this.requiredAspectsData = sb.toString();
ThaumicEnergistics.LOG.debug("[R] aspectsData={}", this.requiredAspectsData);
} else {
this.requiredAspectsData = "";
ThaumicEnergistics.LOG.debug("[R] aspectsData=empty (no aspects)");
}
} else {
ThaumicEnergistics.LOG.debug("[R] else branch: isClientSide={}, levelClass={}",
level.isClientSide(), level.getClass().getSimpleName());
if (level.isClientSide()) {
ThaumicEnergistics.LOG.debug("[R] Client: not overriding synced values (no arcane recipe on client)");
return;
}
this.requiredAspectsData = "";
this.hasValidRecipe = false;
resultSlot.setRequiredAspects(null);
resultSlot.setWandStack(ItemStack.EMPTY);
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=false (no arcane recipe)");
}
}
public Map<String, Integer> getParsedAspects() {
Map<String, Integer> map = new LinkedHashMap<>();
if (requiredAspectsData.isEmpty()) return map;
for (String part : requiredAspectsData.split(",")) {
String[] kv = part.split("=");
if (kv.length == 2) {
try {
map.put(kv[0], Integer.parseInt(kv[1]));
} catch (NumberFormatException ignored) {
}
}
}
return map;
}
// ========== JEI 配方转移支持 ==========
/**
* 确定给定 slot→ingredient 映射中哪些槽位无法从 ME 网络或玩家背包填充。
* 移植自 CraftingTermMenu.findMissingIngredients。
*/
public MissingIngredientSlots findMissingIngredients(Map<Integer, Ingredient> ingredients) {
Set<Integer> missingSlots = new HashSet<>();
Set<Integer> craftableSlots = new HashSet<>();
var reservedGridAmounts = new Object2IntOpenHashMap<Object>();
var playerItems = getPlayerInventory().items;
var reservedPlayerItems = new int[playerItems.size()];
for (var entry : ingredients.entrySet()) {
var ingredient = entry.getValue();
boolean found = false;
for (int i = 0; i < playerItems.size(); i++) {
if (isPlayerInventorySlotLocked(i)) continue;
var stack = playerItems.get(i);
if (stack.getCount() - reservedPlayerItems[i] > 0 && ingredient.test(stack)) {
reservedPlayerItems[i]++;
found = true;
break;
}
}
if (!found) {
if (hasIngredient(ingredient, reservedGridAmounts)) {
reservedGridAmounts.merge(ingredient, 1, Integer::sum);
found = true;
}
}
if (!found) {
for (var stack : ingredient.getItems()) {
if (isCraftable(stack)) {
craftableSlots.add(entry.getKey());
found = true;
break;
}
}
}
if (!found) {
missingSlots.add(entry.getKey());
}
}
return new MissingIngredientSlots(missingSlots, craftableSlots);
}
/**
* 检查 ME 网络中是否有指定材料的库存(含已预留量)。
* 直接遍历 clientRepo 条目匹配 ingredient,确保不受 linkStatus 时序或 getByIngredient 实现细节影响。
*/
public boolean hasIngredient(Ingredient ingredient, Object2IntOpenHashMap<Object> reservedAmounts) {
for (var slot : getSlots(SlotSemantics.CRAFTING_GRID)) {
var stackInSlot = slot.getItem();
if (!stackInSlot.isEmpty() && ingredient.test(stackInSlot)) {
var reservedAmount = reservedAmounts.getOrDefault(slot, 0);
if (stackInSlot.getCount() > reservedAmount) {
reservedAmounts.merge(slot, 1, Integer::sum);
return true;
}
}
}
// 直接遍历 clientRepo 检查 ME 网络仓库
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var entry : clientRepo.getAllEntries()) {
if (!(entry.getWhat() instanceof AEItemKey itemKey)) continue;
if (entry.getStoredAmount() <= 0) continue;
if (!ingredient.test(itemKey.toStack())) continue;
var reservedAmount = reservedAmounts.getOrDefault(entry, 0);
if (entry.getStoredAmount() - reservedAmount >= 1) {
reservedAmounts.merge(entry, 1, Integer::sum);
return true;
}
}
}
return false;
}
/**
* 检查物品是否可在 ME 网络中自动合成。
* 移植自 CraftingTermMenu.isCraftable。
*/
private boolean isCraftable(ItemStack itemStack) {
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var stack : clientRepo.getAllEntries()) {
if (AEItemKey.matches(stack.getWhat(), itemStack) && stack.isCraftable()) {
return true;
}
}
}
return false;
}
public record MissingIngredientSlots(Set<Integer> missingSlots, Set<Integer> craftableSlots) {
public int totalSize() {
return missingSlots.size() + craftableSlots.size();
}
public boolean anyMissingOrCraftable() {
return anyMissing() || anyCraftable();
}
public boolean anyMissing() {
return !missingSlots.isEmpty();
}
public boolean anyCraftable() {
return !craftableSlots.isEmpty();
}
}
}
@@ -0,0 +1,357 @@
package thaumicenergistics.common.container;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import appeng.core.definitions.AEItems;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
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.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.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.tiles.TileDistillationPatternEncoder;
import thaumicenergistics.init.ModMenuTypes;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
private static final int PLAYER_INV_Y = 152;
private static final int HOTBAR_Y = 210;
private static final int PLAYER_INV_X = 8;
private static final int SLOT_SOURCE_X = 15;
private static final int SLOT_SOURCE_Y = 69;
private static final int SLOT_ASPECTS_X = 65;
private static final int SLOT_ASPECTS_Y = 24;
private static final int SLOT_ASPECTS_COUNT = 6;
private static final int SLOT_SELECTED_X = 116;
private static final int SLOT_SELECTED_Y = 69;
private static final int SLOT_BLANK_X = 146;
private static final int SLOT_BLANK_Y = 75;
private static final int SLOT_ENCODED_X = 146;
private static final int SLOT_ENCODED_Y = 113;
private static final int PLAYER_SLOTS = 36;
private static final int ASPECT_SLOT_START = 36;
private static final int SELECTED_SLOT = 42;
private static final int SOURCE_SLOT = 43;
private static final int BLANK_SLOT = 44;
private static final int ENCODED_SLOT = 45;
private final TileDistillationPatternEncoder encoder;
private final Inventory playerInv;
private final ItemStackHandler aspectInventory = new ItemStackHandler(SLOT_ASPECTS_COUNT + 1);
private Aspect[] cachedAspects = new Aspect[0];
private int selectedAspectIndex = -1;
/** 上次解析的源物品,用于判定是否需要重新解析 aspects(避免每帧调用 getObjectAspects)。 */
private ItemStack lastSourceItem = ItemStack.EMPTY;
public ContainerDistillationPatternEncoder(int id, Inventory inv) {
this(id, inv, (TileDistillationPatternEncoder) null);
}
public ContainerDistillationPatternEncoder(int id, Inventory inv, RegistryFriendlyByteBuf buf) {
this(id, inv, (TileDistillationPatternEncoder) null);
}
public ContainerDistillationPatternEncoder(int id, Inventory inv, TileDistillationPatternEncoder encoder) {
super(ModMenuTypes.DISTILLATION_ENCODER.get(), id);
this.encoder = encoder;
this.playerInv = inv;
for (int r = 0; r < 3; r++)
for (int c = 0; c < 9; c++)
addSlot(new Slot(inv, c + r * 9 + 9, PLAYER_INV_X + c * 18, PLAYER_INV_Y + r * 18));
for (int c = 0; c < 9; c++)
addSlot(new Slot(inv, c, PLAYER_INV_X + c * 18, HOTBAR_Y));
for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) {
int y = SLOT_ASPECTS_Y + i * 18;
addSlot(new SlotItemHandler(aspectInventory, i, SLOT_ASPECTS_X, y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
}
addSlot(new SlotItemHandler(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
if (encoder != null) {
// 源物品槽:绑定 Tile 槽 0(持久化到方块 NBT,关闭 GUI 不丢失)
addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) {
@Override
public int getMaxStackSize() { return 1; }
});
addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) {
@Override
public boolean mayPlace(ItemStack s) { return true; }
@Override
public int getMaxStackSize() { return 64; }
});
addSlot(new SlotItemHandler(encoder.inventory, TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, SLOT_ENCODED_X, SLOT_ENCODED_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
});
} else {
// 客户端兜底:三个槽各自独立(避免共享槽导致放入互相覆盖)
ItemStackHandler dummy = new ItemStackHandler(3);
addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_SOURCE_ITEM, SLOT_SOURCE_X, SLOT_SOURCE_Y) {
@Override
public int getMaxStackSize() { return 1; }
});
addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, SLOT_BLANK_X, SLOT_BLANK_Y) {
@Override
public boolean mayPlace(ItemStack s) { return true; }
@Override
public int getMaxStackSize() { return 64; }
});
addSlot(new SlotItemHandler(dummy, TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, SLOT_ENCODED_X, SLOT_ENCODED_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
});
}
}
/** 源物品槽内容。服务端从 Tile 槽 0 读(权威);客户端从槽同步内容读(encoder 为 null)。 */
public ItemStack getSourceItem() {
if (encoder != null) {
return encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_SOURCE_ITEM);
}
if (SOURCE_SLOT >= 0 && SOURCE_SLOT < slots.size()) {
return slots.get(SOURCE_SLOT).getItem();
}
return ItemStack.EMPTY;
}
/**
* 源物品的 aspects:源物品变化(含 NBT/扫描数据)时才调用 {@code getObjectAspects} 重新解析,
* 否则返回缓存——每帧渲染调用仅做 O(1) 物品比较,不频繁请求 TC。
*/
public Aspect[] getSourceAspects() {
ItemStack source = getSourceItem();
if (!ItemStack.isSameItemSameComponents(source, lastSourceItem)) {
lastSourceItem = source.copy();
if (source.isEmpty()) {
cachedAspects = new Aspect[0];
if (selectedAspectIndex != -1) selectedAspectIndex = -1;
} else {
// 玩家必须已用魔导透镜扫描过"该物品"才显示其要素(等价 1.7.10 ScanManager 物品 hash 检查;
// 不能只看 hasDiscoveredAspect——玩家可能已发现全部要素但未扫描此物品)
if (!ThaumometerScanManager.hasBeenScanned(playerInv.player, source)) {
cachedAspects = new Aspect[0];
} else {
AspectList aspects = ThaumcraftApiHelper.getObjectAspects(source);
if (aspects == null || aspects.size() == 0) {
cachedAspects = new Aspect[0];
} else {
// 排序保证客户端/服务端解析顺序一致(GUI 第 X 格 = 编码输出的第 X 个要素)
Aspect[] arr = aspects.getAspects();
Arrays.sort(arr, Comparator.comparing(a -> a.getTag() == null ? "" : a.getTag()));
cachedAspects = arr;
}
}
if (selectedAspectIndex >= cachedAspects.length) selectedAspectIndex = -1;
}
}
return cachedAspects;
}
private void updateAspects() {
// 统一走缓存解析逻辑
getSourceAspects();
// aspect 槽只读显示,图标由 GUI 绘制(保持槽空)
for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) {
aspectInventory.setStackInSlot(i, ItemStack.EMPTY);
}
}
private void clearAspectSlots() {
for (int i = 0; i <= SLOT_ASPECTS_COUNT; i++) {
aspectInventory.setStackInSlot(i, ItemStack.EMPTY);
}
selectedAspectIndex = -1;
}
public Aspect[] getCachedAspects() {
return cachedAspects;
}
public int getSelectedAspectIndex() {
return selectedAspectIndex;
}
public Aspect getSelectedAspect() {
if (selectedAspectIndex >= 0 && selectedAspectIndex < cachedAspects.length) {
return cachedAspects[selectedAspectIndex];
}
return null;
}
/**
* Encode 按钮可用的条件:源物品已配置 + 已选要素 + 空白样板槽有样板 + 成品槽为空。
* 服务端从 Tile 读(权威);客户端 encoder 为 null,从同步的槽内容判断。
*/
public boolean canEncode() {
if (getSelectedAspect() == null) return false;
if (getSourceItem().isEmpty()) return false;
if (encoder != null) {
if (encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS).isEmpty()) return false;
return encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN).isEmpty();
}
if (BLANK_SLOT < 0 || BLANK_SLOT >= slots.size() || slots.get(BLANK_SLOT).getItem().isEmpty()) return false;
return ENCODED_SLOT >= 0 && ENCODED_SLOT < slots.size() && slots.get(ENCODED_SLOT).getItem().isEmpty();
}
public void onEncodePattern() {
if (encoder == null) return;
Aspect selected = getSelectedAspect();
ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
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 (source.isEmpty()) return;
if (blankSlot.isEmpty()) return;
// 验证必须是 AE2 空白样板,否则不消耗不编码(日志定位 is() 运行时行为)
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;
}
if (!encodedSlot.isEmpty()) return;
blankSlot.shrink(1);
if (blankSlot.isEmpty()) {
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS, ItemStack.EMPTY);
}
AEItemKey sourceKey = AEItemKey.of(source);
GenericStack input = new GenericStack(sourceKey, 1);
// 蒸馏语义:源物品 → 选中源质,输出数量 = 源物品中该要素的数量(如骨头食欲=2 → 输出食欲×2)
ResourceLocation aspectId = TCReflection.getAspectId(selected);
if (aspectId == null) return;
AspectList objAspects = ThaumcraftApiHelper.getObjectAspects(source);
long amount = (objAspects != null) ? objAspects.getAmount(selected) : 1;
if (amount < 1) amount = 1;
GenericStack output = new GenericStack(AEssentiaKey.of(aspectId), amount);
ItemStack encoded = PatternDetailsHelper.encodeProcessingPattern(
List.of(input),
List.of(output)
);
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, encoded);
}
@Override
public void clicked(int slotId, int dragType, ClickType clickType, Player player) {
if (slotId >= 0 && slotId < slots.size()) {
Slot slot = slots.get(slotId);
if (slotId == SOURCE_SLOT) {
// 标准拖放处理(SlotItemHandler 已限制槽内 1 个),刷新 aspects
super.clicked(slotId, dragType, clickType, player);
updateAspects();
ThaumicEnergistics.LOG.info("[DE] source slot clicked: item={}, count={}, aspects={}",
getSourceItem().getItem().getDescriptionId(), getSourceItem().getCount(), getSourceAspects().length);
return;
}
if (slotId >= ASPECT_SLOT_START && slotId < ASPECT_SLOT_START + SLOT_ASPECTS_COUNT) {
int aspectIdx = slotId - ASPECT_SLOT_START;
// 始终记录用户选择(越界由 getSelectedAspect 校验),避免因 cachedAspects 状态导致服务端选择丢失
selectedAspectIndex = aspectIdx;
ThaumicEnergistics.LOG.info("[DE] aspect clicked: idx={}, cached={}, selected={}",
aspectIdx, cachedAspects.length, selectedAspectIndex);
// selected 槽留空,图标由 GUI 绘制
aspectInventory.setStackInSlot(SELECTED_SLOT - ASPECT_SLOT_START, ItemStack.EMPTY);
return;
}
}
super.clicked(slotId, dragType, clickType, player);
}
@Override
public boolean clickMenuButton(Player player, int id) {
if (id == 0) {
onEncodePattern();
return true;
}
return false;
}
@Override
public ItemStack quickMoveStack(Player player, int idx) {
Slot slot = slots.get(idx);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack stack = slot.getItem();
ItemStack copy = stack.copy();
if (idx < PLAYER_SLOTS) {
// 空白样板/其他物品 shift → 尝试放入 blank 槽(mayPlace 已放宽,Encode 时验证)
if (!moveItemStackTo(stack, BLANK_SLOT, BLANK_SLOT + 1, false)) return ItemStack.EMPTY;
} else if (idx == BLANK_SLOT || idx == ENCODED_SLOT) {
if (!moveItemStackTo(stack, 0, PLAYER_SLOTS, false)) return ItemStack.EMPTY;
} else {
return ItemStack.EMPTY;
}
if (stack.isEmpty()) slot.set(ItemStack.EMPTY);
else slot.setChanged();
return copy;
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public void removed(Player player) {
super.removed(player);
if (encoder != null && !encoder.getLevel().isClientSide) {
encoder.setChanged();
}
}
}
@@ -0,0 +1,20 @@
package thaumicenergistics.common.container;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumicenergistics.init.ModMenuTypes;
/** 源质元件终端的容器。基于网格的源质视图 + 元件槽。 */
public class ContainerEssentiaCellTerminal extends ThEContainerBase {
final ItemStackHandler cellSlot = new ItemStackHandler(1);
final ItemStackHandler viewSlot = new ItemStackHandler(1);
public ContainerEssentiaCellTerminal(int id, Inventory inv) {
super(ModMenuTypes.ESSENTIA_TERMINAL.get(), id, inv, 2);
addSlot(new SlotItemHandler(cellSlot, 0, 26, 16));
addSlot(new SlotItemHandler(viewSlot, 0, 134, 16));
}
public ContainerEssentiaCellTerminal(int id, Inventory inv, RegistryFriendlyByteBuf buf) { this(id, inv); }
}
@@ -0,0 +1,159 @@
package thaumicenergistics.common.container;
import java.util.List;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.items.ItemEssentiaCell;
import thaumicenergistics.common.tiles.TileEssentiaCellWorkbench;
import thaumicenergistics.init.ModMenuTypes;
/**
* 源质元件工作台容器:cell 槽绑定到方块实体(持久化),并提供分区编辑操作。
* 对应 1.7.10 的 {@code ContainerEssentiaCellWorkbench},分区数据存储在 cell 的 AE2 CellConfig 上。
*/
public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
/** cell 槽编号。 */
public static final int SLOT_CELL = 0;
/** 玩家背包起始编号(1..27 背包,28..36 热栏)。 */
private static final int PLAYER_INV_START = 1;
private static final int PLAYER_INV_END = 28;
private static final int HOTBAR_START = 28;
private static final int HOTBAR_END = 37;
private final TileEssentiaCellWorkbench workbench;
/** 兜底用空库存(workbench 为 null 时)。 */
private final IItemHandler fallbackCell = new ItemStackHandler(1);
public ContainerEssentiaCellWorkbench(int id, Inventory inv) {
this(id, inv, (TileEssentiaCellWorkbench) null);
}
public ContainerEssentiaCellWorkbench(int id, Inventory inv, TileEssentiaCellWorkbench workbench) {
super(ModMenuTypes.ESSENTIA_CELL_WORKBENCH.get(), id);
this.workbench = workbench;
IItemHandler cellHandler = workbench != null ? workbench.getCellInventory() : fallbackCell;
addSlot(new SlotItemHandler(cellHandler, 0, 152, 8));
for (int r = 0; r < 3; r++) for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c + r * 9 + 9, 8 + c * 18, 169 + r * 18));
for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c, 8 + c * 18, 227));
}
/** 客户端打开菜单用(IMenuTypeExtension 注册);workbench 为 null,分区操作走 C2S 到服务端。 */
public ContainerEssentiaCellWorkbench(int id, Inventory inv, net.minecraft.network.RegistryFriendlyByteBuf buf) {
this(id, inv);
}
// ===== 分区操作(由 GUI / 网络调用,服务端执行) =====
public boolean hasCell() {
return workbench != null && workbench.hasCell();
}
public List<ResourceLocation> getPartitionAspects() {
if (workbench == null || !workbench.hasCell()) return List.of();
return ItemEssentiaCell.getPartitionAspects(workbench.getCell());
}
public boolean addAspectToPartition(ResourceLocation aspectId) {
if (workbench == null || !workbench.hasCell()) return false;
boolean changed = ItemEssentiaCell.addAspectToPartition(workbench.getCell(), aspectId);
if (changed) workbench.setChanged();
return changed;
}
public boolean removeAspectFromPartition(ResourceLocation aspectId) {
if (workbench == null || !workbench.hasCell()) return false;
boolean changed = ItemEssentiaCell.removeAspectFromPartition(workbench.getCell(), aspectId);
if (changed) workbench.setChanged();
return changed;
}
public boolean replaceAspectInPartition(ResourceLocation from, ResourceLocation to) {
if (workbench == null || !workbench.hasCell()) return false;
boolean changed = ItemEssentiaCell.replaceAspectInPartition(workbench.getCell(), from, to);
if (changed) workbench.setChanged();
return changed;
}
public void clearPartitioning() {
if (workbench == null || !workbench.hasCell()) return;
ItemEssentiaCell.clearPartitioning(workbench.getCell());
workbench.setChanged();
}
public void partitionToContents() {
if (workbench == null || !workbench.hasCell()) return;
ItemEssentiaCell.partitionToContents(workbench.getCell());
workbench.setChanged();
}
/**
* 从物品中提取第一个源质方面(罐子/法杖等 IEssentiaContainerItem)。
*/
public static ResourceLocation getAspectFromItem(ItemStack stack) {
if (stack == null || stack.isEmpty()) return null;
if (stack.getItem() instanceof IEssentiaContainerItem containerItem) {
AspectList aspects = containerItem.getAspects(stack);
if (aspects != null && aspects.size() > 0) {
Aspect first = aspects.getAspects()[0];
if (first != null) return TCReflection.getAspectId(first);
}
}
return null;
}
// ===== 快捷移动 / 生命周期 =====
@Override
public ItemStack quickMoveStack(Player player, int idx) {
if (workbench == null || idx < 0 || idx >= slots.size()) return ItemStack.EMPTY;
Slot slot = slots.get(idx);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack stack = slot.getItem();
ItemStack original = stack.copy();
if (idx == SLOT_CELL) {
// cell 槽 → 玩家背包
if (!moveItemStackTo(stack, PLAYER_INV_START, HOTBAR_END, true)) return ItemStack.EMPTY;
} else if (idx < HOTBAR_END) {
// 玩家背包 → cell 槽(仅当 cell 槽空且物品是源质元件)
if (stack.getItem() instanceof ItemEssentiaCell && workbench.getCell().isEmpty()) {
if (!moveItemStackTo(stack, SLOT_CELL, SLOT_CELL + 1, false)) return ItemStack.EMPTY;
} else {
// 含源质物品 → 尝试添加分区(物品不消费,仅添加分区)
ResourceLocation aspectId = getAspectFromItem(stack);
if (aspectId != null && addAspectToPartition(aspectId)) {
return original; // 物品留在原槽
}
return ItemStack.EMPTY;
}
}
if (stack.isEmpty()) {
slot.set(ItemStack.EMPTY);
} else {
slot.setChanged();
}
return original;
}
@Override
public boolean stillValid(Player player) {
return workbench == null || workbench.hasLevel() && player.level().getBlockEntity(workbench.getBlockPos()) == workbench;
}
}
@@ -0,0 +1,85 @@
package thaumicenergistics.common.container;
import appeng.api.config.Settings;
import appeng.api.stacks.AEKey;
import appeng.api.util.IConfigManager;
import appeng.api.util.KeyTypeSelection;
import appeng.api.util.KeyTypeSelectionHost;
import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.UpgradeableMenu;
import appeng.menu.interfaces.KeyTypeSelectionMenu;
import appeng.menu.interfaces.KeyTypeSelectionMenu.SyncedKeyTypes;
import appeng.menu.slot.FakeSlot;
import thaumicenergistics.common.parts.EssentiaExportBusPart;
import thaumicenergistics.init.ModMenuTypes;
public class ContainerEssentiaExportBus extends UpgradeableMenu<EssentiaExportBusPart>
implements KeyTypeSelectionMenu {
@GuiSync(50)
public long storedEnergy;
@GuiSync(51)
public SyncedKeyTypes exportKeyTypes = new SyncedKeyTypes();
public ContainerEssentiaExportBus(int id, net.minecraft.world.entity.player.Inventory ip, EssentiaExportBusPart host) {
super(ModMenuTypes.ESSENTIA_EXPORT_BUS.get(), id, ip, host);
registerClientAction("clear", this::clear);
}
@Override
protected void setupConfig() {
var inv = getHost().getConfig().createMenuWrapper();
for (int i = 0; i < 18; i++) {
this.addSlot(new FakeSlot(inv, i), SlotSemantics.CONFIG);
}
}
@Override
public boolean isSlotEnabled(int idx) {
return true;
}
@Override
protected void loadSettingsFromHost(IConfigManager cm) {
if (cm.hasSetting(Settings.REDSTONE_CONTROLLED)) {
this.setRedStoneMode(cm.getSetting(Settings.REDSTONE_CONTROLLED));
}
}
@Override
public void broadcastChanges() {
super.broadcastChanges();
if (!isServerSide()) return;
this.storedEnergy = getHost().getStoredEnergy();
if (getHost() instanceof KeyTypeSelectionHost selectionHost) {
var enabled = selectionHost.getKeyTypeSelection().enabled();
if (!exportKeyTypes.keyTypes().equals(enabled)) {
exportKeyTypes = new SyncedKeyTypes(enabled);
}
}
}
public void clear() {
if (isClientSide()) {
sendClientAction("clear");
} else {
getHost().getConfig().clear();
}
}
public AEKey getConfiguredKey(int slot) {
return getHost().getConfig().getKey(slot);
}
@Override
public KeyTypeSelection getServerKeyTypeSelection() {
return ((KeyTypeSelectionHost) getHost()).getKeyTypeSelection();
}
@Override
public SyncedKeyTypes getClientKeyTypeSelection() {
return exportKeyTypes;
}
}
@@ -0,0 +1,57 @@
package thaumicenergistics.common.container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
import appeng.api.util.KeyTypeSelectionHost;
import appeng.core.definitions.AEItems;
import appeng.menu.implementations.UpgradeableMenu;
import appeng.menu.interfaces.KeyTypeSelectionMenu;
import appeng.menu.interfaces.KeyTypeSelectionMenu.SyncedKeyTypes;
import appeng.api.util.KeyTypeSelection;
import thaumicenergistics.common.parts.EssentiaImportBusPart;
public class ContainerEssentiaImportBus extends UpgradeableMenu<EssentiaImportBusPart>
implements KeyTypeSelectionMenu {
@appeng.menu.guisync.GuiSync(20)
public SyncedKeyTypes importKeyTypes = new SyncedKeyTypes();
public ContainerEssentiaImportBus(MenuType<?> menuType, int id, Inventory ip, EssentiaImportBusPart host) {
super(menuType, id, ip, host);
}
@Override
protected void setupConfig() {
addExpandableConfigSlots(getHost().getConfig(), 2, 9, 5);
}
@Override
public boolean isSlotEnabled(int idx) {
final int upgrades = getUpgrades().getInstalledUpgrades(AEItems.CAPACITY_CARD);
return upgrades > idx;
}
@Override
public void broadcastChanges() {
super.broadcastChanges();
if (isServerSide()) {
if (getHost() instanceof KeyTypeSelectionHost selectionHost) {
var enabled = selectionHost.getKeyTypeSelection().enabled();
if (!importKeyTypes.keyTypes().equals(enabled)) {
importKeyTypes = new SyncedKeyTypes(enabled);
}
}
}
}
@Override
public KeyTypeSelection getServerKeyTypeSelection() {
return ((KeyTypeSelectionHost) getHost()).getKeyTypeSelection();
}
@Override
public SyncedKeyTypes getClientKeyTypeSelection() {
return importKeyTypes;
}
}
@@ -0,0 +1,99 @@
package thaumicenergistics.common.container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
import appeng.api.config.RedstoneMode;
import appeng.api.config.Settings;
import appeng.api.stacks.AEKey;
import appeng.api.util.IConfigManager;
import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.UpgradeableMenu;
import appeng.menu.slot.FakeSlot;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.parts.EssentiaLevelEmitterPart;
/**
* 源质发信器 Menu — 与 AE2 原版 StorageLevelEmitterMenu 逻辑一致。
* 使用 reportingValue 代替 thresholdLevel,与 AbstractLevelEmitterPart 对齐。
*/
public class ContainerEssentiaLevelEmitter extends UpgradeableMenu<EssentiaLevelEmitterPart> {
private static final String ACTION_SET_REPORTING_VALUE = "setReportingValue";
@GuiSync(10)
public long reportingValue;
@GuiSync(11)
public String trackedAspectId = "";
@GuiSync(12)
public long currentLevel;
public ContainerEssentiaLevelEmitter(MenuType<?> menuType, int id, Inventory ip, EssentiaLevelEmitterPart host) {
super(menuType, id, ip, host);
registerClientAction(ACTION_SET_REPORTING_VALUE, Long.class, this::setReportingValue);
}
@Override
protected void setupConfig() {
var inv = getHost().getConfig().createMenuWrapper();
this.addSlot(new FakeSlot(inv, 0), SlotSemantics.CONFIG);
}
@Override
public boolean isSlotEnabled(int idx) {
return true;
}
@Override
protected void loadSettingsFromHost(IConfigManager cm) {
if (cm.hasSetting(Settings.REDSTONE_EMITTER)) {
this.setRedStoneMode(cm.getSetting(Settings.REDSTONE_EMITTER));
}
}
@Override
public void broadcastChanges() {
super.broadcastChanges();
if (!isServerSide()) return;
EssentiaLevelEmitterPart part = getHost();
long hostReporting = part.getReportingValue();
if (this.reportingValue != hostReporting) {
this.reportingValue = hostReporting;
}
long hostCurrent = part.getCurrentLevel();
if (this.currentLevel != hostCurrent) {
this.currentLevel = hostCurrent;
}
AEssentiaKey key = part.getConfiguredEssentiaKey();
String aspectId = key != null ? key.getId().toString() : "";
if (!this.trackedAspectId.equals(aspectId)) {
this.trackedAspectId = aspectId;
}
// 红石模式由 IConfigManager 自动同步
}
public AEKey getConfiguredKey() {
return getHost().getConfig().getKey(0);
}
public void setReportingValue(long value) {
if (isClientSide()) {
sendClientAction(ACTION_SET_REPORTING_VALUE, value);
} else {
getHost().setReportingValue(value);
}
}
public Aspect getTrackedAspect() {
if (trackedAspectId.isEmpty()) return null;
return Aspect.getAspect(trackedAspectId);
}
}
@@ -0,0 +1,165 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
import appeng.api.config.AccessRestriction;
import appeng.api.config.FuzzyMode;
import appeng.api.config.Settings;
import appeng.api.config.StorageFilter;
import appeng.api.config.YesNo;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.api.util.IConfigManager;
import appeng.core.definitions.AEItems;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.UpgradeableMenu;
import thaumicenergistics.common.parts.EssentiaStorageBusPart;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 严格参照 AE2 StorageBusMenu。
* 直接继承 UpgradeableMenu<EssentiaStorageBusPart>getHost().getConfig()、getHost().getUpgrades()、getHost().getConfigManager()
* 均来自 IOBusPart 原生实现,无需自行实现库存。
*/
public class ContainerEssentiaStorageBus extends UpgradeableMenu<EssentiaStorageBusPart> {
private static final String ACTION_CLEAR = "clear";
private static final String ACTION_PARTITION = "partition";
@GuiSync(3)
public AccessRestriction rwMode = AccessRestriction.READ_WRITE;
@GuiSync(4)
public StorageFilter storageFilter = StorageFilter.EXTRACTABLE_ONLY;
@GuiSync(7)
public YesNo filterOnExtract = YesNo.YES;
@GuiSync(8)
public Component connectedTo;
public ContainerEssentiaStorageBus(MenuType<?> menuType, int id, Inventory ip, EssentiaStorageBusPart host) {
super(menuType, id, ip, host);
registerClientAction(ACTION_CLEAR, this::clear);
registerClientAction(ACTION_PARTITION, this::partition);
this.connectedTo = getConnectedDescription(host);
}
@Override
protected void setupConfig() {
// 2行固定实格 + 5行可选淡色虚格 = 63格,完全对齐 AE2 StorageBusMenu.setupConfig()
addExpandableConfigSlots(getHost().getConfig(), 2, 9, 5);
}
@Override
public boolean isSlotEnabled(int idx) {
// 1张容量升级卡启用1行虚格(idx=第idx组可选行),和AE2一致
final int upgrades = getUpgrades().getInstalledUpgrades(AEItems.CAPACITY_CARD);
return upgrades > idx;
}
@Override
public void broadcastChanges() {
super.broadcastChanges();
this.connectedTo = getConnectedDescription(getHost());
}
@Override
protected void loadSettingsFromHost(IConfigManager cm) {
this.setFuzzyMode(cm.getSetting(Settings.FUZZY_MODE));
this.setReadWriteMode(cm.getSetting(Settings.ACCESS));
this.setStorageFilter(cm.getSetting(Settings.STORAGE_FILTER));
this.setFilterOnExtract(cm.getSetting(Settings.FILTER_ON_EXTRACT));
}
public void clear() {
if (isClientSide()) { sendClientAction(ACTION_CLEAR); return; }
getHost().getConfig().clear();
this.broadcastChanges();
}
public void partition() {
if (isClientSide()) { sendClientAction(ACTION_PARTITION); return; }
var inv = getHost().getConfig();
Iterator<AEKey> it = Collections.emptyIterator();
// ============= ✅ 修复3根因:IOBusPart 里没有叫 getInternalHandler() 的方法 =============
// AE2 StorageBusPart extends IOBusPart implements IStorageProvider 自己加了 getInternalHandler
// 我们的 EssentiaStorageBusPart 没有显式实现这个方法 → 反射软查,拿不到就跳过分区(不崩)
Object storage = null;
try {
try {
storage = getHost().getClass().getMethod("getInternalHandler").invoke(getHost());
} catch (NoSuchMethodException ignored) {
storage = getHost().getClass().getMethod("getStorage").invoke(getHost());
}
} catch (Throwable ignored) {}
if (storage != null) {
try {
Object availableStacks = storage.getClass().getMethod("getAvailableStacks").invoke(storage);
if (availableStacks instanceof Iterable<?> iter) {
@SuppressWarnings("unchecked")
Iterator<AEKey> keyIt = ((Iterable<AEKey>) iter).iterator();
it = keyIt;
}
} catch (Throwable ignored) {}
}
inv.beginBatch();
try {
for (int x = 0; x < inv.size(); x++) {
if (it.hasNext() && this.isSlotEnabled(x / 9 - 2)) {
inv.setStack(x, new GenericStack(it.next(), 1));
} else {
inv.setStack(x, null);
}
}
} finally {
inv.endBatch();
}
this.broadcastChanges();
}
public boolean supportsFuzzySearch() { return hasUpgrade(AEItems.FUZZY_CARD); }
public AccessRestriction getReadWriteMode() { return this.rwMode; }
private void setReadWriteMode(AccessRestriction v) { this.rwMode = v; }
public StorageFilter getStorageFilter() { return this.storageFilter; }
private void setStorageFilter(StorageFilter v) { this.storageFilter = v; }
public YesNo getFilterOnExtract() { return this.filterOnExtract; }
public void setFilterOnExtract(YesNo v) { this.filterOnExtract = v; }
public Component getConnectedTo() { return connectedTo; }
public FuzzyMode getFuzzyMode() { return this.fzMode; }
private static Component getConnectedDescription(EssentiaStorageBusPart p) {
java.lang.reflect.Method m = METHOD_CACHE.computeIfAbsent(p.getClass(), c -> {
try { return c.getMethod("getConnectedToDescription"); }
catch (NoSuchMethodException e) { return NO_METHOD; }
});
if (m == NO_METHOD) return null;
try {
return (Component) m.invoke(p);
} catch (Throwable t) {
return null;
}
}
/** 反射方法缓存哨兵:computeIfAbsent 返回非 null 才缓存(避免每次 getMethod)。 */
private static final java.lang.reflect.Method NO_METHOD;
static {
try {
NO_METHOD = Object.class.getMethod("toString");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
/** 反射方法缓存:broadcastChanges 每 tick 调用 getConnectedDescription,避免每次 getMethod。 */
private static final Map<Class<?>, java.lang.reflect.Method> METHOD_CACHE = new ConcurrentHashMap<>();
}
@@ -0,0 +1,19 @@
package thaumicenergistics.common.container;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumicenergistics.init.ModMenuTypes;
/** 源质谐振仓的容器。1 个燃料槽 + 玩家背包。 */
public class ContainerEssentiaVibrationChamber extends ThEContainerBase {
final ItemStackHandler fuelSlot = new ItemStackHandler(1);
public ContainerEssentiaVibrationChamber(int id, Inventory inv) {
super(ModMenuTypes.EVC.get(), id, inv, 1);
addSlot(new SlotItemHandler(fuelSlot, 0, 80, 42));
}
public ContainerEssentiaVibrationChamber(int id, Inventory inv, RegistryFriendlyByteBuf buf) { this(id, inv); }
}
@@ -0,0 +1,531 @@
package thaumicenergistics.common.container;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.Level;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.ThaumcraftApi;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.IArcaneRecipe;
import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
import thaumicenergistics.common.tiles.TileKnowledgeInscriber;
import thaumicenergistics.init.ModItems;
import thaumicenergistics.init.ModMenuTypes;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.ThaumicEnergistics;
public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
public enum CoreSaveState {
Disabled_InvalidRecipe,
Disabled_CoreFull,
Disabled_MissingCore,
Enabled_Save,
Enabled_Delete
}
private static final int MAXIMUM_PATTERNS = HandlerKnowledgeCore.MAXIMUM_STORED_PATTERNS;
private static final int PLAYER_INV_Y = 162;
private static final int HOTBAR_Y = 220;
private static final int PLAYER_INV_X = 8;
private static final int KCORE_SLOT_X = 186;
private static final int KCORE_SLOT_Y = 8;
private static final int PATTERN_SLOT_X = 26;
private static final int PATTERN_SLOT_Y = 18;
private static final int PATTERN_ROWS = 3;
private static final int PATTERN_COLS = 7;
private static final int PATTERN_SPACING = 18;
private static final int CRAFTING_SLOT_X = 26;
private static final int CRAFTING_SLOT_Y = 90;
private static final int CRAFTING_ROWS = 3;
private static final int CRAFTING_COLS = 3;
private static final int CRAFTING_GRID_SIZE = 9;
private static final int CRAFTING_SPACING = 18;
private static final int RESULT_SLOT_X = 116;
private static final int RESULT_SLOT_Y = 108;
private final HandlerKnowledgeCore kCoreHandler = new HandlerKnowledgeCore();
private final TileKnowledgeInscriber tile;
private final Inventory playerInv;
private final ItemStackHandler kCoreInventory = new ItemStackHandler(1) {
@Override
public boolean isItemValid(int slot, ItemStack stack) {
return stack.is(ModItems.KNOWLEDGE_CORE.get());
}
@Override
protected void onContentsChanged(int slot) {
onKCoreChanged();
}
};
private final ItemStackHandler patternInventory = new ItemStackHandler(MAXIMUM_PATTERNS);
private final ItemStackHandler craftingInventory = new ItemStackHandler(CRAFTING_GRID_SIZE);
private final ItemStackHandler resultInventory = new ItemStackHandler(1);
private final SimpleContainer craftingContainer = new SimpleContainer(CRAFTING_GRID_SIZE);
private IArcaneRecipe activeRecipe = null;
private final DataSlot saveStateSlot;
public ContainerKnowledgeInscriber(int id, Inventory inv) {
this(id, inv, (TileKnowledgeInscriber) null);
}
public ContainerKnowledgeInscriber(int id, Inventory inv, RegistryFriendlyByteBuf buf) {
this(id, inv, (TileKnowledgeInscriber) null);
}
public ContainerKnowledgeInscriber(int id, Inventory inv, TileKnowledgeInscriber tile) {
super(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), id);
this.tile = tile;
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 c = 0; c < 9; c++)
addSlot(new Slot(inv, c + r * 9 + 9, PLAYER_INV_X + c * 18, PLAYER_INV_Y + r * 18));
for (int c = 0; c < 9; c++)
addSlot(new Slot(inv, c, PLAYER_INV_X + c * 18, HOTBAR_Y));
addSlot(new SlotItemHandler(kCoreInventory, 0, KCORE_SLOT_X, KCORE_SLOT_Y) {
@Override
public int getMaxStackSize() { return 1; }
});
initPatternSlots();
initCraftingSlots();
addSlot(new SlotItemHandler(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
});
saveStateSlot = addDataSlot(DataSlot.standalone());
saveStateSlot.set(0);
if (tile != null && tile.hasKCore()) {
ItemStack slotStack = tile.getKCore().copy();
kCoreInventory.setStackInSlot(0, slotStack);
kCoreHandler.open(slotStack);
updatePatternSlots();
}
}
private void initPatternSlots() {
for (int r = 0; r < PATTERN_ROWS; r++)
for (int c = 0; c < PATTERN_COLS; c++) {
int idx = r * PATTERN_COLS + c;
int x = PATTERN_SLOT_X + c * PATTERN_SPACING;
int y = PATTERN_SLOT_Y + r * PATTERN_SPACING;
addSlot(new SlotItemHandler(patternInventory, idx, x, y) {
@Override
public boolean mayPlace(ItemStack s) { return false; }
@Override
public boolean mayPickup(Player p) { return false; }
@Override
public int getMaxStackSize() { return 1; }
});
}
}
private void initCraftingSlots() {
for (int r = 0; r < CRAFTING_ROWS; r++)
for (int c = 0; c < CRAFTING_COLS; c++) {
int idx = r * CRAFTING_COLS + c;
int x = CRAFTING_SLOT_X + c * CRAFTING_SPACING;
int y = CRAFTING_SLOT_Y + r * CRAFTING_SPACING;
addSlot(new GhostSlot(craftingContainer, idx, x, y));
}
}
private void syncKCoreToTile() {
if (tile != null) {
ItemStack kCore = kCoreInventory.getStackInSlot(0);
tile.inventory.setStackInSlot(TileKnowledgeInscriber.KCORE_SLOT, kCore.copy());
tile.setChanged();
}
}
private void onKCoreChanged() {
ItemStack kCore = kCoreInventory.getStackInSlot(0);
if (kCore.isEmpty()) {
kCoreHandler.close();
clearPatternSlots();
} else if (!kCoreHandler.isHandlingCore(kCore)) {
kCoreHandler.open(kCore);
updatePatternSlots();
}
syncKCoreToTile();
}
private void clearPatternSlots() {
for (int i = 0; i < MAXIMUM_PATTERNS; i++) {
patternInventory.setStackInSlot(i, ItemStack.EMPTY);
}
}
private void updatePatternSlots() {
ArrayList<ItemStack> results = kCoreHandler.hasCore()
? kCoreHandler.getStoredOutputs()
: new ArrayList<>();
Iterator<ItemStack> it = results.iterator();
for (int i = 0; i < MAXIMUM_PATTERNS; i++) {
if (it.hasNext()) {
patternInventory.setStackInSlot(i, it.next().copy());
} else {
patternInventory.setStackInSlot(i, ItemStack.EMPTY);
}
}
}
private CoreSaveState getSaveState() {
if (!kCoreHandler.hasCore()) {
return CoreSaveState.Disabled_MissingCore;
}
if (activeRecipe == null) {
return CoreSaveState.Disabled_InvalidRecipe;
}
ItemStack result = resultInventory.getStackInSlot(0);
if (result.isEmpty()) {
return CoreSaveState.Disabled_InvalidRecipe;
}
boolean isNew = !kCoreHandler.hasPatternFor(result);
if (isNew) {
return kCoreHandler.hasRoomToStorePattern()
? CoreSaveState.Enabled_Save
: CoreSaveState.Disabled_CoreFull;
} else {
return CoreSaveState.Enabled_Delete;
}
}
private void updateRecipe() {
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingContainer.setItem(i, craftingInventory.getStackInSlot(i).isEmpty()
? ItemStack.EMPTY
: craftingInventory.getStackInSlot(i).copy());
}
activeRecipe = findArcaneRecipe();
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: activeRecipe={}", activeRecipe);
if (activeRecipe != null) {
ItemStack output = activeRecipe.getCraftingResult(craftingContainer);
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: output={}", output.isEmpty() ? "EMPTY" : output.getItem().toString());
resultInventory.setStackInSlot(0, output);
} else {
ThaumicEnergistics.LOG.info("[KLGE] updateRecipe: no recipe matched → EMPTY");
resultInventory.setStackInSlot(0, ItemStack.EMPTY);
}
sendSaveState();
}
private IArcaneRecipe findArcaneRecipe() {
Level level = playerInv.player.level();
if (level == null) {
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: level is null");
return null;
}
var recipes = ThaumcraftApi.getCraftingRecipes();
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: total recipes={}", recipes.size());
int arcaneCount = 0;
for (Object recipe : recipes) {
if (recipe instanceof IArcaneRecipe arcaneRecipe) {
arcaneCount++;
try {
if (gridMatches(arcaneRecipe, craftingContainer)) {
ThaumicEnergistics.LOG.info("[KLGE] findArcaneRecipe: MATCH FOUND! recipe={}",
arcaneRecipe);
return arcaneRecipe;
}
} 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;
}
private boolean gridMatches(IArcaneRecipe recipe, SimpleContainer inv) {
if (recipe instanceof ShapedArcaneRecipe shaped) {
for (int x = 0; x <= 3 - shaped.width; x++) {
for (int y = 0; y <= 3 - shaped.height; y++) {
if (checkShaped(shaped, inv, x, y, false) || checkShaped(shaped, inv, x, y, true)) {
return true;
}
}
}
return false;
}
if (recipe instanceof ShapelessArcaneRecipe shapeless) {
ArrayList<Object> required = new ArrayList<>(shapeless.getInput());
for (int slot = 0; slot < 9; slot++) {
ItemStack stack = inv.getItem(slot);
if (stack.isEmpty()) continue;
Iterator<Object> it = required.iterator();
boolean inRecipe = false;
while (it.hasNext()) {
Object next = it.next();
if (ingredientMatches(next, stack)) {
inRecipe = true;
it.remove();
break;
}
}
if (!inRecipe) return false;
}
return required.isEmpty();
}
return recipe.matches(inv, playerInv.player.level(), playerInv.player);
}
private boolean checkShaped(ShapedArcaneRecipe recipe, SimpleContainer inv, int startX, int startY, boolean mirror) {
for (int x = 0; x < 3; x++) {
for (int y = 0; y < 3; y++) {
int subX = x - startX;
int subY = y - startY;
Object target = null;
if (subX >= 0 && subY >= 0 && subX < recipe.width && subY < recipe.height) {
target = mirror
? recipe.input[recipe.width - subX - 1 + subY * recipe.width]
: recipe.input[subX + subY * recipe.width];
}
if (ingredientMatches(target, inv.getItem(x + y * 3))) continue;
return false;
}
}
return true;
}
private static boolean ingredientMatches(Object target, ItemStack input) {
if (target == null) return input.isEmpty();
if (target instanceof ItemStack t) return stackMatches(t, input);
if (target instanceof List<?> list) {
for (Object alt : list) {
if (alt instanceof ItemStack s && stackMatches(s, input)) return true;
}
return false;
}
if (target instanceof TagKey<?> tag && tag.isFor(Registries.ITEM)) {
@SuppressWarnings("unchecked")
TagKey<Item> itemTag = (TagKey<Item>) tag;
return !input.isEmpty() && input.is(itemTag);
}
return false;
}
private static boolean stackMatches(ItemStack target, ItemStack input) {
if (input.isEmpty()) return target.isEmpty();
if (target.isEmpty()) return false;
return target.getItem() == input.getItem()
&& (target.getComponentsPatch().isEmpty() || ItemStack.isSameItemSameComponents(target, input));
}
private void sendSaveState() {
if (tile != null && tile.getLevel() != null && !tile.getLevel().isClientSide) {
saveStateSlot.set(getSaveState().ordinal());
}
}
public void onSaveDelete() {
CoreSaveState state = getSaveState();
if (state == CoreSaveState.Enabled_Save) {
savePattern();
} else if (state == CoreSaveState.Enabled_Delete) {
deletePattern();
}
}
private void savePattern() {
if (activeRecipe == null) return;
ItemStack[] inputs = new ItemStack[CRAFTING_GRID_SIZE];
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
ItemStack stack = craftingInventory.getStackInSlot(i);
inputs[i] = stack.isEmpty() ? ItemStack.EMPTY : stack.copy();
}
AspectList aspects = activeRecipe.getAspects(craftingContainer);
if (aspects == null) {
aspects = activeRecipe.getAspects();
}
ItemStack result = resultInventory.getStackInSlot(0).copy();
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs);
kCoreHandler.addPattern(pattern);
updatePatternSlots();
sendSaveState();
syncKCoreToTile();
}
private void deletePattern() {
ItemStack result = resultInventory.getStackInSlot(0);
ArcaneCraftingPattern pattern = kCoreHandler.getPatternForItem(result);
if (pattern != null) {
kCoreHandler.removePattern(pattern);
updatePatternSlots();
sendSaveState();
syncKCoreToTile();
}
}
public void onClearGrid() {
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingInventory.setStackInSlot(i, ItemStack.EMPTY);
craftingContainer.setItem(i, ItemStack.EMPTY);
}
activeRecipe = null;
resultInventory.setStackInSlot(0, ItemStack.EMPTY);
sendSaveState();
}
public CoreSaveState getCurrentSaveState() {
return CoreSaveState.values()[saveStateSlot.get()];
}
@Override
public void clicked(int slotId, int dragType, ClickType clickType, Player player) {
if (slotId >= 0 && slotId < slots.size()) {
Slot slot = slots.get(slotId);
if (slot instanceof GhostSlot ghostSlot) {
int gridIndex = ghostSlot.index;
ItemStack carried = getCarried();
ItemStack newStack = carried.isEmpty()
? ItemStack.EMPTY
: 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);
if (player.level().isClientSide) {
ThaumicEnergistics.LOG.info("[KLGE] CLIENT branch → onCraftingChangedClient + C2S");
onCraftingChangedClient();
PacketDistributor.sendToServer(
new KnowledgeInscriberGhostSlotPacket(containerId, gridIndex, newStack));
} else {
ThaumicEnergistics.LOG.info("[KLGE] SERVER branch → onCraftingChanged");
onCraftingChanged();
}
return;
}
}
super.clicked(slotId, dragType, clickType, player);
}
public void setGhostSlotOnServer(int slotIndex, ItemStack stack) {
if (slotIndex < 0 || slotIndex >= CRAFTING_GRID_SIZE) return;
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);
int slotId = 37 + MAXIMUM_PATTERNS + slotIndex;
if (slotId < slots.size() && slots.get(slotId) instanceof GhostSlot) {
slots.get(slotId).set(s);
}
onCraftingChanged();
}
private void onCraftingChangedClient() {
ThaumicEnergistics.LOG.info("[KLGE] onCraftingChangedClient");
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy());
}
updateRecipe();
}
private void onCraftingChanged() {
ThaumicEnergistics.LOG.info("[KLGE] onCraftingChanged");
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
craftingInventory.setStackInSlot(i, craftingContainer.getItem(i).copy());
}
updateRecipe();
}
@Override
public boolean clickMenuButton(Player player, int id) {
if (id == 0) {
onSaveDelete();
return true;
} else if (id == 1) {
onClearGrid();
return true;
}
return false;
}
@Override
public ItemStack quickMoveStack(Player player, int idx) {
Slot slot = slots.get(idx);
if (!slot.hasItem()) return ItemStack.EMPTY;
ItemStack stack = slot.getItem();
ItemStack copy = stack.copy();
if (idx < 36) {
if (stack.is(ModItems.KNOWLEDGE_CORE.get())) {
if (!moveItemStackTo(stack, 36, 37, false)) return ItemStack.EMPTY;
} else {
return ItemStack.EMPTY;
}
} else if (idx == 36) {
if (!moveItemStackTo(stack, 0, 36, false)) return ItemStack.EMPTY;
} else {
return ItemStack.EMPTY;
}
if (stack.isEmpty()) slot.set(ItemStack.EMPTY);
else slot.setChanged();
return copy;
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public void removed(Player player) {
super.removed(player);
syncKCoreToTile();
kCoreHandler.close();
}
}
@@ -0,0 +1,109 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* 奥术装配器 GUI。
*
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
* 第1块 主矩形:(0,0) → 176×197
* 第2块 右上不规则:(179,0) → 上部32w(0~15y),下部55w(16~103y)
* 第3块 右下护甲区:(180,129) → 68×68
* 第4块 底部7个Vis条:每个 4×16 像素,起始(42,202),步长1842-45, 60-63, 78-81...
*
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
*/
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png");
// ===== 第4块 Vis条 =====
private static final int VIS_BAR_X = 42;
private static final int VIS_BAR_Y = 202;
private static final int VIS_BAR_W = 4;
private static final int VIS_BAR_H = 16;
private static final int VIS_BAR_STEP = 18;
// ===== 6原质 =====
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) {
super(menu, inv, title);
this.imageWidth = 254;
this.imageHeight = 226;
this.inventoryLabelY = 115 - 12;
this.titleLabelY = 5;
}
@Override
protected void renderBg(GuiGraphics g, float partialTicks, int mx, int my) {
int left = (this.width - this.imageWidth) / 2;
int top = (this.height - this.imageHeight) / 2;
// ===== 分块绘制纹理(仿1.7.10bar区域不参与blit =====
// 第1块:主矩形 176×197
g.blit(TEX, left, top, 0, 0, 176, 197);
// 第2块:右上不规则区域(上部 32×16 + 下部 55×88
g.blit(TEX, left + 179, top, 179, 0, 32, 16);
g.blit(TEX, left + 179, top + 16, 179, 16, 55, 88);
// 第3块:右下正方形 68×68
g.blit(TEX, left + 180, top + 129, 180, 129, 68, 68);
// ===== 第4块:Vis条(纹理裁剪方式,仿1.7.10 drawVisBar =====
TileArcaneAssembler tile = this.menu.assembler;
final int maxCvis = 187 * 10;
for (int i = 0; i < 7; i++) {
int bx = left + VIS_BAR_X + i * VIS_BAR_STEP;
int by = top + VIS_BAR_Y;
float ratio = 0f;
if (tile != null) {
if (i < 6) {
int cvis = tile.getStoredVis() != null ? tile.getStoredVis().getAmount(VIS_ASPECTS[i]) : 0;
ratio = Math.min(1f, (float) cvis / maxCvis);
} else {
ratio = tile.getCraftProgress();
}
}
// 空bar暗色背景(满bar不在此处,由纹理裁剪提供)
g.fill(bx, by, bx + VIS_BAR_W, by + VIS_BAR_H, 0x77222222);
// 从纹理Y=202处裁剪满bar,按高度百分比叠加
int fillHeight = Math.round(VIS_BAR_H * ratio);
if (fillHeight > 0) {
int srcX = VIS_BAR_X + i * VIS_BAR_STEP;
int srcY = VIS_BAR_Y + (VIS_BAR_H - fillHeight);
g.blit(TEX, bx, by + (VIS_BAR_H - fillHeight), srcX, srcY, VIS_BAR_W, fillHeight);
}
// bar边框
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 - 1, by - 1, bx + VIS_BAR_W + 1, by, 0xAA000000);
g.fill(bx - 1, by + VIS_BAR_H, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
}
}
@Override
public void render(GuiGraphics g, int mx, int my, float pt) {
this.renderBackground(g, mx, my, pt);
super.render(g, mx, my, pt);
this.renderTooltip(g, mx, my);
}
}
@@ -0,0 +1,86 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.menu.slot.AppEngCraftingSlot;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.client.gui.AspectGuiRenderer;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.style.ScreenStyle;
import java.util.Map;
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
/** 6 个要素图标位置(左上角坐标) */
private static final int[][] ASPECT_POSITIONS = {
{10, 95}, // 左列 第1行
{10, 112}, // 左列 第2行
{10, 128}, // 左列 第3行
{80, 95}, // 右列 第1行
{80, 112}, // 右列 第2行
{80, 128}, // 右列 第3行
};
private static final int ICON_SIZE = 14;
public GuiArcaneCraftingTerminal(ContainerArcaneCraftingTerminal menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
/** 合成结果槽锚点 Y(首次计算缓存,避免每帧遍历 slots)。 */
private int cachedAnchorY = -1;
@Override
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
super.drawFG(guiGraphics, offsetX, offsetY, mouseX, mouseY);
if (!this.menu.hasValidRecipe) return;
Map<String, Integer> aspectMap = this.menu.getParsedAspects();
if (aspectMap.isEmpty()) return;
// 获取合成结果槽的 Y 位置作为锚点(槽位置不变,首次计算后缓存,避免每帧遍历)
int anchorY;
if (cachedAnchorY < 0) {
for (var slot : this.menu.slots) {
// CRAFTING_RESULT 槽在 style JSON 中 bottom:140
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
cachedAnchorY = slot.y;
break;
}
}
// 若找不到锚点则回退到硬编码值
if (cachedAnchorY < 0) cachedAnchorY = 95;
}
anchorY = cachedAnchorY;
// 要素圆圈 Y = 锚点 + 偏移(-16/1/17,对应纹理 95/112/128
int[] aspectOffsets = { -16, 1, 17 };
int index = 0;
for (Map.Entry<String, Integer> entry : aspectMap.entrySet()) {
if (index >= 6) break;
Aspect aspect = Aspect.getAspect(entry.getKey());
if (aspect != null) {
int col = index / 3; // 0=左列, 1=右列
int row = index % 3; // 0~2
int x = (col == 0) ? 10 : 80;
int y = anchorY + aspectOffsets[row];
AspectGuiRenderer.draw(guiGraphics, aspect, x, y, ICON_SIZE, 1.0f);
String amountStr = String.valueOf(entry.getValue());
int textX = x + ICON_SIZE - this.font.width(amountStr) + 1;
int textY = y + ICON_SIZE - 6;
guiGraphics.drawString(this.font, amountStr, textX, textY, 0xFFFFFF, true);
index++;
}
}
}
}
@@ -0,0 +1,88 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.client.gui.AspectGuiRenderer;
import thaumicenergistics.ThaumicEnergistics;
public class GuiDistillationPatternEncoder extends AbstractContainerScreen<ContainerDistillationPatternEncoder> {
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/distillation_encoder.png");
private static final int GUI_WIDTH = 176;
private static final int GUI_HEIGHT = 234;
/** Encode 按钮引用,按配置状态动态启用/禁用。 */
private Button encodeButton;
public GuiDistillationPatternEncoder(ContainerDistillationPatternEncoder menu, Inventory inv, Component title) {
super(menu, inv, title);
this.imageWidth = GUI_WIDTH;
this.imageHeight = GUI_HEIGHT;
this.inventoryLabelY = this.imageHeight - 94;
}
@Override
protected void init() {
super.init();
int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2;
// Encode 按钮:对齐 1.7.10146,94),位于 encoded 槽上方,不覆盖 selected 槽 (116,69)
// 仅当配置完成(源物品+选中要素+空白样板就绪)时可点击
encodeButton = Button.builder(Component.literal("Encode"), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0);
}).pos(left + 140, top + 94).size(34, 16).build();
addRenderableWidget(encodeButton);
}
@Override
protected void renderBg(GuiGraphics g, float pt, int mx, int my) {
int x = (width - imageWidth) / 2;
int y = (height - imageHeight) / 2;
g.blit(TEX, x, y, 0, 0, imageWidth, imageHeight);
// 绘制源物品的 6 个 aspect 图标(aspect 槽 (65,24) 起,每格 18px);实时缓存解析(源物品变化才调 TC)
Aspect[] aspects = menu.getSourceAspects();
for (int i = 0; i < 6; i++) {
if (i < aspects.length && aspects[i] != null) {
AspectGuiRenderer.draw(g, aspects[i], x + 65, y + 24 + i * 18, 16, 1.0f);
}
}
// 源物品非空但解析不到要素(未扫描)→ 提示
if (!menu.getSourceItem().isEmpty() && aspects.length == 0) {
g.drawCenteredString(font,
Component.translatable("thaumicenergistics.gui.distillation_encoder.not_scanned"),
x + 73, y + 78, 0xFFAAAAAA);
}
// 绘制选中的 aspect 图标(selected 槽 (116,69)
Aspect selected = menu.getSelectedAspect();
if (selected != null) {
AspectGuiRenderer.draw(g, selected, x + 116, y + 69, 16, 1.0f);
}
}
@Override
public void containerTick() {
super.containerTick();
// Encode 按钮仅在配置完成(源物品+选中要素+空白样板就绪)时可点击
if (encodeButton != null) {
encodeButton.active = menu.canEncode();
}
}
@Override
public void render(GuiGraphics g, int mx, int my, float pt) {
super.render(g, mx, my, pt);
renderTooltip(g, mx, my);
}
}
@@ -0,0 +1,15 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumicenergistics.ThaumicEnergistics;
public class GuiEssentiaCellTerminal extends AbstractContainerScreen<ContainerEssentiaCellTerminal> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID,"textures/gui/essentia_terminal.png");
public GuiEssentiaCellTerminal(ContainerEssentiaCellTerminal m, Inventory inv, Component t) { super(m,inv,t); imageWidth=176; imageHeight=166; inventoryLabelY=imageHeight-94; }
@Override protected void renderBg(GuiGraphics g, float pt, int mx, int my) { int x=(width-imageWidth)/2,y=(height-imageHeight)/2; g.blit(TEX,x,y,0,0,imageWidth,imageHeight); }
@Override public void render(GuiGraphics g, int mx, int my, float pt) { super.render(g,mx,my,pt); renderTooltip(g,mx,my); }
}
@@ -0,0 +1,133 @@
package thaumicenergistics.common.container;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import appeng.api.client.AEKeyRendering;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.network.CellWorkbenchC2SPacket;
/**
* 源质元件工作台 GUI:显示 63 个分区格(9×7),支持点击添加/移除/替换源质分区,两个分区按钮。
* 对应 1.7.10 的 {@code GuiEssentiaCellWorkbench}。
*/
public class GuiEssentiaCellWorkbench extends AbstractContainerScreen<ContainerEssentiaCellWorkbench> {
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "textures/gui/essentia_cell_workbench.png");
private static final int GUI_WIDTH = 176, GUI_HEIGHT = 251;
/** 分区格布局常量(JEI ghost 拖拽也使用,故公开)。 */
public static final int WIDGET_POS_X = 7, WIDGET_POS_Y = 28;
public static final int WIDGETS_PER_ROW = 9, WIDGET_ROWS = 7, NUMBER_OF_WIDGETS = 63;
public static final int WIDGET_SIZE = 18;
/** 当前显示的分区列表(大小 63,null = 空分区格)。由 S2C 同步更新。 */
private final List<ResourceLocation> partitionSlots = new ArrayList<>(Collections.nCopies(NUMBER_OF_WIDGETS, null));
/** 上次检测到的 cell 槽内容,用于在放入/拿走 cell 时触发分区刷新。 */
private ItemStack lastCellStack = ItemStack.EMPTY;
public GuiEssentiaCellWorkbench(ContainerEssentiaCellWorkbench menu, Inventory inv, Component title) {
super(menu, inv, title);
imageWidth = GUI_WIDTH;
imageHeight = GUI_HEIGHT;
inventoryLabelY = imageHeight - 94;
}
@Override
protected void init() {
super.init();
// 分区为当前(P)与清除分区(C)按钮,位置对应 1.7.10(GUI 左外侧)
addRenderableWidget(Button.builder(Component.literal("P"), b -> CellWorkbenchC2SPacket.sendPartitionToContents())
.bounds(leftPos - 18, topPos + 8, 18, 18).build());
addRenderableWidget(Button.builder(Component.literal("C"), b -> CellWorkbenchC2SPacket.sendClear())
.bounds(leftPos - 18, topPos + 28, 18, 18).build());
// 打开时请求服务端分区列表
CellWorkbenchC2SPacket.sendRequestPartitions();
}
@Override
protected void renderBg(GuiGraphics g, float pt, int mx, int my) {
int x = (width - imageWidth) / 2, y = (height - imageHeight) / 2;
g.blit(TEX, x, y, 0, 0, imageWidth, imageHeight);
for (int i = 0; i < NUMBER_OF_WIDGETS; i++) {
ResourceLocation id = partitionSlots.get(i);
if (id != null) {
int col = i % WIDGETS_PER_ROW, row = i / WIDGETS_PER_ROW;
AEKeyRendering.drawInGui(minecraft, g,
x + WIDGET_POS_X + col * WIDGET_SIZE,
y + WIDGET_POS_Y + row * WIDGET_SIZE,
AEssentiaKey.of(id));
}
}
}
@Override
public boolean mouseClicked(double mx, double my, int btn) {
for (int i = 0; i < NUMBER_OF_WIDGETS; i++) {
int col = i % WIDGETS_PER_ROW, row = i / WIDGETS_PER_ROW;
int wx = leftPos + WIDGET_POS_X + col * WIDGET_SIZE;
int wy = topPos + WIDGET_POS_Y + row * WIDGET_SIZE;
if (mx >= wx && mx < wx + WIDGET_SIZE && my >= wy && my < wy + WIDGET_SIZE) {
handlePartitionClick(i);
return true;
}
}
return super.mouseClicked(mx, my, btn);
}
/**
* 点击分区格:空 + 手持含源质物品 → 添加;有源质 + 空手 → 移除;有源质 + 不同源质 → 替换。
*/
private void handlePartitionClick(int index) {
ResourceLocation current = index < partitionSlots.size() ? partitionSlots.get(index) : null;
ResourceLocation held = ContainerEssentiaCellWorkbench.getAspectFromItem(menu.getCarried());
if (current == null && held != null) {
CellWorkbenchC2SPacket.sendAddAspect(held);
} else if (current != null && held == null) {
CellWorkbenchC2SPacket.sendRemoveAspect(current);
} else if (current != null && held != null && !current.equals(held)) {
CellWorkbenchC2SPacket.sendReplaceAspect(current, held);
}
}
/**
* 服务端同步分区列表(S2C handler 调用)。
*/
public void updatePartitions(List<ResourceLocation> list) {
partitionSlots.clear();
if (list.size() > NUMBER_OF_WIDGETS) {
partitionSlots.addAll(list.subList(0, NUMBER_OF_WIDGETS));
} else {
partitionSlots.addAll(list);
}
for (int i = partitionSlots.size(); i < NUMBER_OF_WIDGETS; i++) {
partitionSlots.add(null);
}
}
@Override
public void render(GuiGraphics g, int mx, int my, float pt) {
// 检测 cell 槽变化:放入已配置 cell → 重新请求分区;拿走 cell → 立即清空显示
ItemStack cell = menu.slots.get(0).getItem();
if (!ItemStack.matches(cell, lastCellStack)) {
lastCellStack = cell.copy();
if (cell.isEmpty()) {
updatePartitions(List.of());
} else {
CellWorkbenchC2SPacket.sendRequestPartitions();
}
}
super.render(g, mx, my, pt);
renderTooltip(g, mx, my);
}
}
@@ -0,0 +1,75 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.api.config.FuzzyMode;
import appeng.api.config.RedstoneMode;
import appeng.api.config.SchedulingMode;
import appeng.api.config.Settings;
import appeng.api.config.YesNo;
import appeng.api.util.KeyTypeSelectionHost;
import appeng.client.gui.implementations.UpgradeableScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.KeyTypeSelectionButton;
import appeng.client.gui.widgets.ServerSettingToggleButton;
import appeng.client.gui.widgets.SettingToggleButton;
import appeng.core.definitions.AEItems;
import appeng.core.localization.GuiText;
import thaumicenergistics.init.ModItems;
public class GuiEssentiaExportBus extends UpgradeableScreen<ContainerEssentiaExportBus> {
private final SettingToggleButton<RedstoneMode> redstoneMode;
private final SettingToggleButton<FuzzyMode> fuzzyMode;
private final SettingToggleButton<YesNo> craftMode;
private final SettingToggleButton<SchedulingMode> schedulingMode;
public GuiEssentiaExportBus(ContainerEssentiaExportBus menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
setTextContent("dialog_title", ModItems.ESSENTIA_EXPORT_BUS.get().getDescription());
if (menu.getHost() instanceof KeyTypeSelectionHost) {
addToLeftToolbar(
KeyTypeSelectionButton.create(this, menu.getHost(), GuiText.ConfigureImportedTypes.text()));
}
this.redstoneMode = new ServerSettingToggleButton<>(Settings.REDSTONE_CONTROLLED, RedstoneMode.IGNORE);
addToLeftToolbar(this.redstoneMode);
this.fuzzyMode = new ServerSettingToggleButton<>(Settings.FUZZY_MODE, FuzzyMode.IGNORE_ALL);
addToLeftToolbar(this.fuzzyMode);
if (menu.getHost().getConfigManager().hasSetting(Settings.CRAFT_ONLY)) {
this.craftMode = new ServerSettingToggleButton<>(Settings.CRAFT_ONLY, YesNo.NO);
addToLeftToolbar(this.craftMode);
} else {
this.craftMode = null;
}
if (menu.getHost().getConfigManager().hasSetting(Settings.SCHEDULING_MODE)) {
this.schedulingMode = new ServerSettingToggleButton<>(Settings.SCHEDULING_MODE, SchedulingMode.DEFAULT);
addToLeftToolbar(this.schedulingMode);
} else {
this.schedulingMode = null;
}
}
@Override
protected void updateBeforeRender() {
super.updateBeforeRender();
this.redstoneMode.set(menu.getRedStoneMode());
this.redstoneMode.setVisibility(menu.hasUpgrade(AEItems.REDSTONE_CARD));
this.fuzzyMode.set(menu.getFuzzyMode());
this.fuzzyMode.setVisibility(menu.hasUpgrade(AEItems.FUZZY_CARD));
if (this.craftMode != null) {
this.craftMode.set(menu.getCraftingMode());
this.craftMode.setVisibility(menu.hasUpgrade(AEItems.CRAFTING_CARD));
}
if (this.schedulingMode != null) {
this.schedulingMode.set(menu.getSchedulingMode());
}
}
}
@@ -0,0 +1,63 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.api.config.FuzzyMode;
import appeng.api.config.RedstoneMode;
import appeng.api.config.Settings;
import appeng.api.config.YesNo;
import appeng.api.util.KeyTypeSelectionHost;
import appeng.client.gui.implementations.UpgradeableScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.KeyTypeSelectionButton;
import appeng.client.gui.widgets.ServerSettingToggleButton;
import appeng.client.gui.widgets.SettingToggleButton;
import appeng.core.definitions.AEItems;
import appeng.core.localization.GuiText;
import thaumicenergistics.init.ModItems;
public class GuiEssentiaImportBus extends UpgradeableScreen<ContainerEssentiaImportBus> {
private final SettingToggleButton<RedstoneMode> redstoneMode;
private final SettingToggleButton<FuzzyMode> fuzzyMode;
private final SettingToggleButton<YesNo> craftMode;
public GuiEssentiaImportBus(ContainerEssentiaImportBus menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
setTextContent("dialog_title", ModItems.ESSENTIA_IMPORT_BUS.get().getDescription());
if (menu.getHost() instanceof KeyTypeSelectionHost) {
addToLeftToolbar(
KeyTypeSelectionButton.create(this, menu.getHost(), GuiText.ConfigureImportedTypes.text()));
}
this.redstoneMode = new ServerSettingToggleButton<>(Settings.REDSTONE_CONTROLLED, RedstoneMode.IGNORE);
addToLeftToolbar(this.redstoneMode);
this.fuzzyMode = new ServerSettingToggleButton<>(Settings.FUZZY_MODE, FuzzyMode.IGNORE_ALL);
addToLeftToolbar(this.fuzzyMode);
if (menu.getHost().getConfigManager().hasSetting(Settings.CRAFT_ONLY)) {
this.craftMode = new ServerSettingToggleButton<>(Settings.CRAFT_ONLY, YesNo.NO);
addToLeftToolbar(this.craftMode);
} else {
this.craftMode = null;
}
}
@Override
protected void updateBeforeRender() {
super.updateBeforeRender();
this.redstoneMode.set(menu.getRedStoneMode());
this.redstoneMode.setVisibility(menu.hasUpgrade(AEItems.REDSTONE_CARD));
this.fuzzyMode.set(menu.getFuzzyMode());
this.fuzzyMode.setVisibility(menu.hasUpgrade(AEItems.FUZZY_CARD));
if (this.craftMode != null) {
this.craftMode.set(menu.getCraftingMode());
this.craftMode.setVisibility(menu.hasUpgrade(AEItems.CRAFTING_CARD));
}
}
}
@@ -0,0 +1,102 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumcraft.api.aspects.Aspect;
import appeng.api.config.RedstoneMode;
import appeng.api.config.Settings;
import appeng.client.gui.NumberEntryType;
import appeng.client.gui.implementations.UpgradeableScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.NumberEntryWidget;
import appeng.client.gui.widgets.ServerSettingToggleButton;
import appeng.client.gui.widgets.SettingToggleButton;
import thaumicenergistics.ThaumicEnergistics;
/**
* 源质发信器 Screen — 与 AE2 原版 StorageLevelEmitterScreen 逻辑一致。
*/
public class GuiEssentiaLevelEmitter extends UpgradeableScreen<ContainerEssentiaLevelEmitter> {
private static final ResourceLocation ASPECT_SLOT_BG =
ThaumicEnergistics.id("textures/gui/essentia_terminal.png");
private final SettingToggleButton<RedstoneMode> redstoneMode;
private final NumberEntryWidget level;
private long lastReportingValue;
private static final int ASPECT_SLOT_X = 137;
private static final int ASPECT_SLOT_Y = 40;
public GuiEssentiaLevelEmitter(ContainerEssentiaLevelEmitter menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
this.redstoneMode = new ServerSettingToggleButton<>(
Settings.REDSTONE_EMITTER, RedstoneMode.HIGH_SIGNAL);
this.addToLeftToolbar(this.redstoneMode);
this.level = widgets.addNumberEntryWidget("level", NumberEntryType.of(menu.getConfiguredKey()));
this.level.setTextFieldStyle(style.getWidget("levelInput"));
this.level.setLongValue(this.menu.reportingValue);
this.level.setOnChange(this::saveReportingValue);
this.level.setOnConfirm(this::onClose);
this.lastReportingValue = this.menu.reportingValue;
}
@Override
protected void updateBeforeRender() {
super.updateBeforeRender();
// 更新数字输入类型(筛选可能已改变)
this.level.setType(NumberEntryType.of(menu.getConfiguredKey()));
this.level.setActive(true);
if (this.lastReportingValue != this.menu.reportingValue) {
this.lastReportingValue = this.menu.reportingValue;
this.level.setLongValue(this.menu.reportingValue);
}
this.redstoneMode.set(this.menu.getRedStoneMode());
this.redstoneMode.setVisibility(true);
}
@Override
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
super.drawFG(guiGraphics, offsetX, offsetY, mouseX, mouseY);
Aspect aspect = this.menu.getTrackedAspect();
if (aspect != null) {
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
public boolean mouseClicked(double mouseX, double mouseY, int button) {
// 右键点击筛选槽清零 reportingValue
if (isPointInRegion(ASPECT_SLOT_X, ASPECT_SLOT_Y, 16, 16, mouseX, mouseY)) {
if (button == 1) {
this.menu.setReportingValue(0);
return true;
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
private void saveReportingValue() {
this.level.getLongValue().ifPresent(menu::setReportingValue);
}
private boolean isPointInRegion(int x, int y, int w, int h, double mx, double my) {
int left = this.leftPos + x;
int top = this.topPos + y;
return mx >= left && mx < left + w && my >= top && my < top + h;
}
}
@@ -0,0 +1,91 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.api.config.ActionItems;
import appeng.api.config.FuzzyMode;
import appeng.client.gui.implementations.UpgradeableScreen;
import appeng.client.gui.style.PaletteColor;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.ActionButton;
import appeng.client.gui.widgets.ServerSettingToggleButton;
import appeng.client.gui.widgets.SettingToggleButton;
import appeng.core.definitions.AEItems;
import appeng.core.localization.GuiText;
import appeng.api.config.AccessRestriction;
import appeng.api.config.Settings;
import appeng.api.config.StorageFilter;
import appeng.api.config.YesNo;
import thaumicenergistics.init.ModItems;
/**
* 源质存储总线GUI。
* 使用 AE2 原生 /screens/storage_bus.json(布局/工具栏/优先级按钮/尺寸完全一致),
* 仅通过 setTextContent("dialog_title", ...) 覆盖标题为"源质存储总线"。
*/
public class GuiEssentiaStorageBus extends UpgradeableScreen<ContainerEssentiaStorageBus> {
private final SettingToggleButton<AccessRestriction> rwMode;
private final SettingToggleButton<StorageFilter> storageFilter;
private final SettingToggleButton<YesNo> filterOnExtract;
private final SettingToggleButton<FuzzyMode> fuzzyMode;
public GuiEssentiaStorageBus(ContainerEssentiaStorageBus menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
// ✅ 覆盖JSON中的 dialog_title 文本(原本是 gui.ae2.StorageBus → "存储总线"
setTextContent("dialog_title", ModItems.ESSENTIA_STORAGE_BUS.get().getDescription());
// 右上角优先级按钮(AE2原生JSON里定义了 openPriority widget
widgets.addOpenPriorityButton();
// 左侧工具栏:清空 / 分区(始终显示)
addToLeftToolbar(new ActionButton(ActionItems.CLOSE, btn -> menu.clear()));
addToLeftToolbar(new ActionButton(ActionItems.COG, btn -> menu.partition()));
// 存储过滤模式(始终显示)
this.storageFilter = new ServerSettingToggleButton<>(Settings.STORAGE_FILTER, StorageFilter.EXTRACTABLE_ONLY);
this.addToLeftToolbar(this.storageFilter);
// 提取时应用过滤(始终显示)
this.filterOnExtract = new ServerSettingToggleButton<>(Settings.FILTER_ON_EXTRACT, YesNo.YES);
this.addToLeftToolbar(this.filterOnExtract);
// 模糊模式(需要模糊升级卡才显示)
this.fuzzyMode = new ServerSettingToggleButton<>(Settings.FUZZY_MODE, FuzzyMode.IGNORE_ALL);
this.addToLeftToolbar(this.fuzzyMode);
// 读写访问模式(始终显示)
this.rwMode = new ServerSettingToggleButton<>(Settings.ACCESS, AccessRestriction.READ_WRITE);
this.addToLeftToolbar(this.rwMode);
}
@Override
protected void updateBeforeRender() {
super.updateBeforeRender();
this.storageFilter.set(this.menu.getStorageFilter());
this.rwMode.set(this.menu.getReadWriteMode());
this.filterOnExtract.set(this.menu.getFilterOnExtract());
this.fuzzyMode.set(this.menu.getFuzzyMode());
this.fuzzyMode.setVisibility(menu.hasUpgrade(AEItems.FUZZY_CARD));
}
@Override
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
super.drawFG(guiGraphics, offsetX, offsetY, mouseX, mouseY);
var poseStack = guiGraphics.pose();
poseStack.pushPose();
poseStack.translate(10, 17, 0);
poseStack.scale(0.6f, 0.6f, 1);
int color = style.getColor(PaletteColor.DEFAULT_TEXT_COLOR).toARGB();
if (menu.getConnectedTo() != null) {
guiGraphics.drawString(font, GuiText.AttachedTo.text(menu.getConnectedTo()), 0, 0, color, false);
} else {
guiGraphics.drawString(font, GuiText.Unattached.text(), 0, 0, color, false);
}
poseStack.popPose();
}
}
@@ -0,0 +1,15 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumicenergistics.ThaumicEnergistics;
public class GuiEssentiaVibrationChamber extends AbstractContainerScreen<ContainerEssentiaVibrationChamber> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID,"textures/gui/essentia_vibration_chamber.png");
public GuiEssentiaVibrationChamber(ContainerEssentiaVibrationChamber m, Inventory inv, Component t) { super(m,inv,t); imageWidth=176; imageHeight=166; inventoryLabelY=imageHeight-94; }
@Override protected void renderBg(GuiGraphics g, float pt, int mx, int my) { int x=(width-imageWidth)/2,y=(height-imageHeight)/2; g.blit(TEX,x,y,0,0,imageWidth,imageHeight); }
@Override public void render(GuiGraphics g, int mx, int my, float pt) { super.render(g,mx,my,pt); renderTooltip(g,mx,my); }
}
@@ -0,0 +1,101 @@
package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import thaumicenergistics.ThaumicEnergistics;
/**
* 知识记录仪 GUI。
* 从 1.7.10 GuiKnowledgeInscriber 移植。
*/
public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnowledgeInscriber> {
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/knowledge_inscriber.png");
private static final int GUI_WIDTH = 210;
private static final int GUI_HEIGHT = 244;
private Button saveButton;
private Button clearButton;
private ContainerKnowledgeInscriber.CoreSaveState saveState =
ContainerKnowledgeInscriber.CoreSaveState.Disabled_MissingCore;
public GuiKnowledgeInscriber(ContainerKnowledgeInscriber menu, Inventory inv, Component title) {
super(menu, inv, title);
this.imageWidth = GUI_WIDTH;
this.imageHeight = GUI_HEIGHT;
this.inventoryLabelY = this.imageHeight - 94;
}
@Override
protected void init() {
super.init();
int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2;
saveButton = Button.builder(Component.literal(""), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0);
}).pos(left + 141, top + 109).size(38, 18).build();
addRenderableWidget(saveButton);
clearButton = Button.builder(Component.literal("C"), btn -> {
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 1);
}).pos(left + 80, top + 89).size(8, 8).build();
addRenderableWidget(clearButton);
}
@Override
protected void containerTick() {
super.containerTick();
ContainerKnowledgeInscriber.CoreSaveState newState = menu.getCurrentSaveState();
if (newState != saveState) {
saveState = newState;
updateSaveButton();
}
}
private void updateSaveButton() {
switch (saveState) {
case Disabled_MissingCore:
saveButton.active = false;
saveButton.setMessage(Component.literal("No Core"));
break;
case Disabled_InvalidRecipe:
saveButton.active = false;
saveButton.setMessage(Component.literal("Invalid"));
break;
case Disabled_CoreFull:
saveButton.active = false;
saveButton.setMessage(Component.literal("Full"));
break;
case Enabled_Save:
saveButton.active = true;
saveButton.setMessage(Component.literal("Save"));
break;
case Enabled_Delete:
saveButton.active = true;
saveButton.setMessage(Component.literal("Delete"));
break;
}
}
@Override
protected void renderBg(GuiGraphics g, float pt, int mx, int my) {
int x = (width - imageWidth) / 2;
int y = (height - imageHeight) / 2;
g.blit(TEX, x, y, 0, 0, imageWidth, imageHeight);
}
@Override
public void render(GuiGraphics g, int mx, int my, float pt) {
super.render(g, mx, my, pt);
renderTooltip(g, mx, my);
}
}
@@ -0,0 +1,28 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.MenuProvider;
import java.util.function.BiFunction;
/** 不依赖AE2 SubMenu 体系的 MenuProvider,给三个总线打开自定义GUI用 */
public record SimpleMenuProvider<T extends AbstractContainerMenu>(
Component title,
MenuType<T> type,
BiFunction<Integer, Inventory, T> factory
) implements MenuProvider {
@Override
public Component getDisplayName() {
return title;
}
@Override
public AbstractContainerMenu createMenu(int id, Inventory playerInv, Player player) {
return factory.apply(id, playerInv);
}
}
@@ -0,0 +1,48 @@
package thaumicenergistics.common.container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
/** 带玩家背包的基础容器。所有 ThE 容器都继承它。 */
public abstract class ThEContainerBase extends AbstractContainerMenu {
protected final Inventory playerInv;
protected final int playerInvStart;
// ===== AE2 标准坐标(源自 storage_bus.json + player_inventory.json=====
// 所有 ThE 容器(无论是否走 UpgradeableMenu)都以此基准对齐。
public static final int AE2_IMAGE_HEIGHT = 253;
public static final int AE2_PLAYER_INV_Y = AE2_IMAGE_HEIGHT - 84; // 169
public static final int AE2_HOTBAR_Y = AE2_IMAGE_HEIGHT - 26; // 227
public static final int AE2_PLAYER_INV_X = 8;
protected ThEContainerBase(MenuType<?> type, int id, Inventory playerInv, int customSlots) {
super(type, id);
this.playerInv = playerInv;
this.playerInvStart = customSlots;
addPlayerInventory(customSlots);
}
private void addPlayerInventory(int start) {
for (int r = 0; r < 3; r++) for (int c = 0; c < 9; c++)
addSlot(new Slot(playerInv, c + r * 9 + 9,
AE2_PLAYER_INV_X + c * 18, AE2_PLAYER_INV_Y + r * 18));
for (int c = 0; c < 9; c++)
addSlot(new Slot(playerInv, c,
AE2_PLAYER_INV_X + c * 18, AE2_HOTBAR_Y));
}
@Override public ItemStack quickMoveStack(Player p, int idx) {
ItemStack r = ItemStack.EMPTY; Slot s = slots.get(idx);
if (s.hasItem()) { ItemStack st = s.getItem(); r = st.copy();
if (idx < playerInvStart) { if (!moveItemStackTo(st, playerInvStart, slots.size(), true)) return ItemStack.EMPTY; }
else if (!moveItemStackTo(st, 0, playerInvStart, false)) return ItemStack.EMPTY;
if (st.isEmpty()) s.set(ItemStack.EMPTY); else s.setChanged();
} return r;
}
@Override public boolean stillValid(Player p) { return true; }
}
@@ -0,0 +1,133 @@
package thaumicenergistics.common.container.slot;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import appeng.api.inventories.InternalInventory;
import appeng.menu.slot.AppEngCraftingSlot;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
/**
* 奥术合成结果槽:在取走合成结果时消耗法杖要素。
* 参考 TC4 1.21.1 ArcaneWorkbenchMenu.onResultTaken + consumeVis。
*/
public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
/** TC4 内部使用 centivis1 vis = 100 centivis */
private static final int CENTIVIS = 100;
private AspectList requiredAspects = null;
private ItemStack wandStack = ItemStack.EMPTY;
private Runnable refreshCallback = null;
private ContainerArcaneCraftingTerminal container = null;
public ArcaneCraftingResultSlot(Player player, InternalInventory craftingGridInv) {
super(player, craftingGridInv);
}
public void setContainer(ContainerArcaneCraftingTerminal container) {
this.container = container;
}
public void setRefreshCallback(Runnable callback) {
this.refreshCallback = callback;
}
public void setRequiredAspects(AspectList aspects) {
this.requiredAspects = aspects;
}
public void setWandStack(ItemStack wandStack) {
this.wandStack = wandStack;
}
@Override
public boolean mayPickup(Player player) {
if (requiredAspects == null || requiredAspects.isEmpty()) {
return super.mayPickup(player);
}
if (wandStack.isEmpty()) return false;
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return false;
}
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
return wand.consumeVisCost(wandStack, player, centivisCost, false, true);
}
@Override
public void onTake(Player player, ItemStack stack) {
// 记录消耗前各合成格中的物品类型,用于消耗后从 ME 网络补充
java.util.Map<Integer, ItemStack> beforeItems = new java.util.HashMap<>();
if (container != null) {
var grid = container.getCraftingMatrix();
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) {
beforeItems.put(i, s.copy());
}
}
}
if (container != null) {
container.setCraftingResultBeingTaken(true);
}
try {
super.onTake(player, stack);
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
if (wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand) {
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
wand.consumeVisCost(wandStack, player, centivisCost, true, true);
}
}
// 从 ME 网络自动补充消耗掉的合成材料
if (container != null && !beforeItems.isEmpty()) {
var grid = container.getCraftingMatrix();
var node = container.getGridNode();
if (node != null && node.getGrid() != null) {
var storage = node.getGrid().getStorageService().getInventory();
var energy = container.getEnergySource();
var actionSource = container.getActionSource();
for (var entry : beforeItems.entrySet()) {
int slot = entry.getKey();
var beforeItem = entry.getValue();
var currentItem = grid.getStackInSlot(slot);
// 如果该格物品变少了(被消耗了),从网络提取补充
if (currentItem.getCount() < beforeItem.getCount()) {
long needed = beforeItem.getCount() - currentItem.getCount();
var key = appeng.api.stacks.AEItemKey.of(beforeItem);
if (key != null) {
long extracted = appeng.api.storage.StorageHelper.poweredExtraction(
energy, storage, key, needed, actionSource);
if (extracted > 0) {
var toPlace = beforeItem.copyWithCount((int) (currentItem.getCount() + extracted));
grid.setItemDirect(slot, toPlace);
}
}
}
}
}
}
} finally {
if (container != null) {
container.setCraftingResultBeingTaken(false);
}
}
if (refreshCallback != null) {
refreshCallback.run();
}
}
}
@@ -0,0 +1,31 @@
package thaumicenergistics.common.container.slot;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
/**
* 幽灵物品槽,用于知识记录仪的合成网格。
* 玩家可以放置任意物品,也可以取回。
*/
public class GhostSlot extends Slot {
public GhostSlot(Container inv, int idx, int x, int y) {
super(inv, idx, x, y);
}
@Override
public boolean mayPlace(ItemStack stack) {
return true;
}
@Override
public boolean mayPickup(Player player) {
return true;
}
@Override
public int getMaxStackSize() {
return 1;
}
}
@@ -0,0 +1,58 @@
package thaumicenergistics.common.entities;
import net.minecraft.resources.ResourceLocation;
import thaumicenergistics.ThaumicEnergistics;
/**
* 傀儡无线背包皮肤枚举。
* 每种皮肤对应一个纹理文件:{@code textures/model/golemBackpack/{texId}.png}。
* 皮肤通过 AE2 Facade 物品右键傀儡来更换。
* 对应 1.7.10 的 {@code ItemGolemWirelessBackpack.BackpackSkins}。
*/
public enum BackpackSkins {
Thaumium("Thaum"),
Stone("Stone"),
Straw("Straw"),
Wood("Wood"),
Flesh("Flesh"),
Clay("Clay"),
Iron("Iron"),
Tallow("Tallow"),
Gold("Gold"),
Diamond("Diamond");
public static final BackpackSkins[] VALUES = values();
/** 纹理文件 ID(不含路径和扩展名) */
private final String texId;
/** 延迟初始化的纹理 ResourceLocation */
private ResourceLocation texture;
BackpackSkins(String texId) {
this.texId = texId;
}
/**
* 获取此皮肤的纹理 ResourceLocation。
* 延迟初始化,避免在枚举构造期间调用 ThaumicEnergistics.MODID。
*/
public ResourceLocation getTextureLocation() {
if (this.texture == null) {
this.texture = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/model/golemBackpack/" + this.texId + ".png");
}
return this.texture;
}
/**
* 从序号获取皮肤,越界时返回默认 Thaumium。
*/
public static BackpackSkins fromOrdinal(int ordinal) {
if (ordinal < 0 || ordinal >= VALUES.length) {
return Thaumium;
}
return VALUES[ordinal];
}
}
@@ -0,0 +1,84 @@
package thaumicenergistics.common.entities;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
/**
* AE2 Facade → 背包皮肤映射。
* 1.7.10 通过 ItemFacade.getBlock()/getMeta() 判断;1.21.1 解析 Facade 物品的底层方块。
* 支持:Thaumium/Tallow/Wood/Flesh/Stone/Straw/Clay/Iron/Gold/Diamond(与 1.7.10 一致)。
*/
public final class FacadeToSkinMapping {
private FacadeToSkinMapping() {}
/**
* 尝试从物品栈推断对应的背包皮肤。
*
* 接受 AE2 Facade 物品或直接的方块物品。
*
* @param stack 玩家手持的物品
* @return 对应的皮肤,若不支持则返回 null
*/
public static BackpackSkins getSkinFromFacade(ItemStack stack) {
if (stack == null || stack.isEmpty()) {
return null;
}
Block block = resolveFacadeBlock(stack);
if (block == null) {
return null;
}
ResourceLocation blockId = BuiltInRegistries.BLOCK.getKey(block);
// Thaumcraft 方块映射
if (blockId.getNamespace().equals("thaumcraft")) {
String path = blockId.getPath();
if (path.contains("thaumium") || path.contains("cosmetic_solid")) {
return BackpackSkins.Thaumium;
}
if (path.contains("tallow")) return BackpackSkins.Tallow;
if (path.contains("greatwood") || path.contains("magical_log")) return BackpackSkins.Wood;
if (path.contains("flesh") || path.contains("taint")) return BackpackSkins.Flesh;
}
// 原版方块映射
if (block == Blocks.STONE) return BackpackSkins.Stone;
if (block == Blocks.HAY_BLOCK) return BackpackSkins.Straw;
if (block == Blocks.BRICKS) return BackpackSkins.Clay;
if (block == Blocks.IRON_BLOCK) return BackpackSkins.Iron;
if (block == Blocks.GOLD_BLOCK) return BackpackSkins.Gold;
if (block == Blocks.DIAMOND_BLOCK) return BackpackSkins.Diamond;
return null;
}
/**
* 从物品栈解析对应方块:BlockItem 或 AE2 Facade(反射接口)。
*/
private static Block resolveFacadeBlock(ItemStack stack) {
// 1) 直接是 BlockItem
if (stack.getItem() instanceof BlockItem blockItem) {
return blockItem.getBlock();
}
// 2) AE2 Facade 物品:尝试通过 IFacadeItem 接口获取纹理方块
try {
if (stack.getItem() instanceof appeng.items.parts.FacadeItem facadeItem) {
net.minecraft.world.level.block.state.BlockState state = facadeItem.getTextureBlockState(stack);
if (state != null) {
return state.getBlock();
}
}
} catch (Throwable ignored) {
// AE2 API 版本可能不兼容,静默忽略
}
return null;
}
}
@@ -0,0 +1,265 @@
package thaumicenergistics.common.entities;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;
import net.minecraft.core.GlobalPos;
import net.minecraft.nbt.NbtOps;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import thaumcraft.common.entities.golem.ThaumcraftGolemEntity;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.items.ItemGolemWirelessBackpack;
import thaumicenergistics.init.ModItems;
/**
* 傀儡无线背包处理器:装备/拆卸/Facade 皮肤更换。
* 对应 1.7.10 的 {@code WirelessGolemHandler} 中装备/Facade 交互部分。
* 网络同步由 {@link thaumicenergistics.common.network.GolemBackpackSyncPacket} 处理。
*/
@EventBusSubscriber(modid = ThaumicEnergistics.MODID)
public final class GolemBackpackHandler {
private static final String NBT_KEY_LINK = "ThEWifiBackpackLink";
/** NBT 键:背包皮肤序号(int */
private static final String NBT_KEY_SKIN = "ThEBackpackSkin";
/** NBT 键:Facade 物品序列化 */
private static final String NBT_KEY_FACADE = "ThEBackpackFacade";
/**
* 傀儡 UUID → 解码后的 home 位置缓存,避免每 tick 重复 GlobalPos.CODEC.parse NBT 解码。
* 使用 WeakHashMapgolem 实体被 GC 后键自动回收,无泄漏;set/clear 时同步失效。
*/
private static final Map<UUID, GlobalPos> LINK_CACHE = Collections.synchronizedMap(new WeakHashMap<>());
private GolemBackpackHandler() {
}
@SubscribeEvent
public static void onEntityInteract(PlayerInteractEvent.EntityInteract event) {
if (!(event.getTarget() instanceof ThaumcraftGolemEntity golem)) {
return;
}
Player player = event.getEntity();
ItemStack held = player.getItemInHand(event.getHand());
if (held.isEmpty()) {
return;
}
// 1) 手持无线背包物品 → 装备背包
if (held.getItem() == ModItems.GOLEM_WIFI_BACKPACK.get()) {
if (event.getLevel().isClientSide()) {
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
return;
}
handleEquipBackpack(golem, player, held, event);
return;
}
// 2) 手持 Golem 铃铛 + 蹲下 → 拆卸背包
if (held.getItem() instanceof thaumcraft.common.items.GolemBellItem) {
if (event.getLevel().isClientSide()) {
return;
}
handleDismantleBackpack(golem, player, event);
return;
}
// 3) 手持 AE2 Facade → 更换皮肤
if (hasBackpack(golem)) {
BackpackSkins skin = FacadeToSkinMapping.getSkinFromFacade(held);
if (skin != null) {
if (event.getLevel().isClientSide()) {
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
return;
}
handleApplyFacade(golem, player, held, skin, event);
}
}
}
private static void handleEquipBackpack(ThaumcraftGolemEntity golem, Player player, ItemStack held,
PlayerInteractEvent.EntityInteract event) {
if (!golem.isOwnedBy(player)) {
return;
}
if (hasBackpack(golem)) {
return;
}
ItemGolemWirelessBackpack backpackItem = (ItemGolemWirelessBackpack) held.getItem();
GlobalPos linkedPos = backpackItem.getLinkedPosition(held);
if (linkedPos == null) {
return;
}
setBackpackLink(golem, linkedPos);
// 默认皮肤为 Thaumium
setBackpackSkin(golem, BackpackSkins.Thaumium);
golem.level().playSound(null, golem.getX(), golem.getY(), golem.getZ(),
SoundEvents.ARMOR_EQUIP_LEATHER, SoundSource.NEUTRAL, 0.5F, 1.0F);
if (!player.isCreative()) {
held.shrink(1);
}
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
}
private static void handleDismantleBackpack(ThaumcraftGolemEntity golem, Player player,
PlayerInteractEvent.EntityInteract event) {
if (!player.isShiftKeyDown()) {
return;
}
if (!golem.isOwnedBy(player)) {
return;
}
GlobalPos linkedPos = getBackpackLink(golem);
if (linkedPos == null) {
return;
}
ItemStack backpackStack = new ItemStack(ModItems.GOLEM_WIFI_BACKPACK.get());
backpackStack.set(appeng.api.ids.AEComponents.WIRELESS_LINK_TARGET, linkedPos);
golem.spawnAtLocation(backpackStack);
// 丢弃旧 Facade(如果有)
ItemStack oldFacade = getFacadeItem(golem);
if (!oldFacade.isEmpty() && !player.isCreative()) {
golem.spawnAtLocation(oldFacade);
}
clearBackpackLink(golem);
clearBackpackSkin(golem);
clearFacadeItem(golem);
golem.level().playSound(null, golem.getX(), golem.getY(), golem.getZ(),
SoundEvents.ARMOR_EQUIP_LEATHER, SoundSource.NEUTRAL, 0.5F, 1.0F);
}
/**
* 处理 Facade 皮肤更换。
* 对应 1.7.10 的 {@code WirelessGolemHandler.customInteraction} 中的 Facade 分支。
*/
private static void handleApplyFacade(ThaumcraftGolemEntity golem, Player player, ItemStack held,
BackpackSkins skin, PlayerInteractEvent.EntityInteract event) {
if (!golem.isOwnedBy(player)) {
return;
}
ItemStack oldFacade = getFacadeItem(golem);
if (!oldFacade.isEmpty() && !player.isCreative()) {
golem.spawnAtLocation(oldFacade);
}
ItemStack facadeCopy = held.copy();
facadeCopy.setCount(1);
setFacadeItem(golem, facadeCopy);
setBackpackSkin(golem, skin);
if (!player.isCreative()) {
held.shrink(1);
}
// 播放音效(对应 1.7.10 的 thaumcraft:cameraticks
golem.level().playSound(null, golem.getX(), golem.getY(), golem.getZ(),
SoundEvents.ARMOR_EQUIP_LEATHER, SoundSource.NEUTRAL, 0.5F, 1.0F);
event.setCancellationResult(InteractionResult.SUCCESS);
event.setCanceled(true);
}
// ========== NBT 操作方法 ==========
public static boolean hasBackpack(ThaumcraftGolemEntity golem) {
return golem.getPersistentData().contains(NBT_KEY_LINK);
}
public static GlobalPos getBackpackLink(ThaumcraftGolemEntity golem) {
var tag = golem.getPersistentData();
if (!tag.contains(NBT_KEY_LINK)) {
LINK_CACHE.remove(golem.getUUID());
return null;
}
GlobalPos cached = LINK_CACHE.get(golem.getUUID());
if (cached != null) {
return cached;
}
var linkTag = tag.getCompound(NBT_KEY_LINK);
GlobalPos decoded = GlobalPos.CODEC.parse(NbtOps.INSTANCE, linkTag).result().orElse(null);
if (decoded != null) {
LINK_CACHE.put(golem.getUUID(), decoded);
}
return decoded;
}
public static void setBackpackLink(ThaumcraftGolemEntity golem, GlobalPos pos) {
var tag = golem.getPersistentData();
GlobalPos.CODEC.encodeStart(NbtOps.INSTANCE, pos)
.result().ifPresent(encoded -> tag.put(NBT_KEY_LINK, encoded));
LINK_CACHE.put(golem.getUUID(), pos);
}
public static void clearBackpackLink(ThaumcraftGolemEntity golem) {
golem.getPersistentData().remove(NBT_KEY_LINK);
LINK_CACHE.remove(golem.getUUID());
}
public static BackpackSkins getBackpackSkin(ThaumcraftGolemEntity golem) {
int ordinal = golem.getPersistentData().getInt(NBT_KEY_SKIN);
return BackpackSkins.fromOrdinal(ordinal);
}
public static void setBackpackSkin(ThaumcraftGolemEntity golem, BackpackSkins skin) {
golem.getPersistentData().putInt(NBT_KEY_SKIN, skin.ordinal());
}
/**
* 清除背包皮肤 NBT。
*/
public static void clearBackpackSkin(ThaumcraftGolemEntity golem) {
golem.getPersistentData().remove(NBT_KEY_SKIN);
}
/**
* 获取保存的 Facade 物品。
*/
public static ItemStack getFacadeItem(ThaumcraftGolemEntity golem) {
var tag = golem.getPersistentData();
if (!tag.contains(NBT_KEY_FACADE)) {
return ItemStack.EMPTY;
}
return ItemStack.parse(golem.level().registryAccess(), tag.getCompound(NBT_KEY_FACADE))
.orElse(ItemStack.EMPTY);
}
/**
* 保存 Facade 物品到 NBT。
*/
public static void setFacadeItem(ThaumcraftGolemEntity golem, ItemStack facade) {
var encoded = (net.minecraft.nbt.CompoundTag) facade.save(golem.level().registryAccess());
golem.getPersistentData().put(NBT_KEY_FACADE, encoded);
}
/**
* 清除 Facade NBT。
*/
public static void clearFacadeItem(ThaumcraftGolemEntity golem) {
golem.getPersistentData().remove(NBT_KEY_FACADE);
}
}
@@ -0,0 +1,323 @@
package thaumicenergistics.common.entities;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.tick.EntityTickEvent;
import net.minecraft.world.Container;
import net.minecraft.world.level.Level;
import net.neoforged.neoforge.fluids.FluidStack;
import org.jetbrains.annotations.Nullable;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.entities.golem.ThaumcraftGolemEntity;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.network.GolemBackpackSyncPacket;
import net.neoforged.neoforge.network.PacketDistributor;
/**
* 傀儡背包无线网络交互的 Tick 驱动 AI。
* 按 1.7.10 的 AI 体系,根据傀儡核心类型执行不同 AE 网络操作:
* FILL(0)/GATHER(2) 物品、LIQUID(5) 流体、ESSENTIA(6) 源质。
*/
@EventBusSubscriber(modid = ThaumicEnergistics.MODID)
public final class GolemBackpackTickHandler {
/**
* 网络操作冷却时间(tick),与 1.7.10 {@code AIAENetworkGolem.NETWORK_COOLDOWN} 一致。
*/
private static final int NETWORK_COOLDOWN = 20;
/**
* 同步状态冷却时间(tick),与 1.7.10 的 18 tick 一致。
*/
private static final int SYNC_COOLDOWN = 18;
private GolemBackpackTickHandler() {
}
@SubscribeEvent
public static void onEntityTick(EntityTickEvent.Post event) {
if (!(event.getEntity() instanceof ThaumcraftGolemEntity golem)) {
return;
}
if (!(golem.level() instanceof ServerLevel)) {
return;
}
if (!GolemBackpackHandler.hasBackpack(golem)) {
return;
}
GlobalPos linkTarget = GolemBackpackHandler.getBackpackLink(golem);
if (linkTarget == null) {
return;
}
// 同步背包连接状态到客户端(每 SYNC_COOLDOWN tick 一次)
syncBackpackStatus(golem, linkTarget);
// 冷却检查:使用傀儡的 persistentData 存储冷却计时器
int cooldown = golem.getPersistentData().getInt("ThEWifiCooldown");
if (cooldown > 0) {
golem.getPersistentData().putInt("ThEWifiCooldown", cooldown - 1);
return;
}
golem.getPersistentData().putInt("ThEWifiCooldown", NETWORK_COOLDOWN);
WirelessAELinkGolem link = new WirelessAELinkGolem(golem, linkTarget);
if (!link.isConnected()) {
return;
}
int maxRate = WirelessAELinkGolem.getMaxItemRate(golem);
int core = golem.getCore();
switch (core) {
case 0 -> doFill(golem, link, maxRate);
case 2 -> doGather(golem, link, maxRate);
case 5 -> doLiquid(golem, link);
case 6 -> doEssentia(golem, link);
default -> {}
}
}
/**
* GATHER 核心 (ID=2): 傀儡手持物品时存入 AE 网络(对应 1.7.10 AIGolemWifiGather)。
*/
private static void doGather(ThaumcraftGolemEntity golem, WirelessAELinkGolem link, int maxRate) {
ItemStack held = golem.carried();
if (held.isEmpty()) {
return;
}
link.depositStack(held, maxRate);
if (held.isEmpty()) {
golem.setCarried(ItemStack.EMPTY);
golem.startActionTimer();
}
}
/**
* FILL 核心 (ID=0): 从 AE 网络提取物品给傀儡,用于填充 home 容器(对应 1.7.10 AIGolemWifiFill)。
* 与 TC 原版 workFill() 的区别:原版从附近标记容器取物,无线背包版直接从 ME 网络取。
*/
private static void doFill(ThaumcraftGolemEntity golem, WirelessAELinkGolem link, int maxRate) {
if (!golem.carried().isEmpty()) {
return;
}
Container homeContainer = getHomeContainer(golem);
if (homeContainer == null) {
return;
}
ItemStack filterToExtract = null;
int extractAmount = 0;
for (int slot = 0; slot < 9; slot++) {
ItemStack filter = golem.getFilter(slot);
if (filter.isEmpty()) {
continue;
}
int desired = Math.max(1, filter.getCount());
int current = countInContainer(homeContainer, filter);
int needed = desired - current;
if (needed <= 0) {
continue;
}
if (!hasRoom(homeContainer, filter, golem.homeFacing())) {
continue;
}
filterToExtract = filter.copyWithCount(1);
extractAmount = Math.min(needed, golem.carryLimit());
break;
}
if (filterToExtract == null) {
return;
}
ItemStack request = filterToExtract.copyWithCount(extractAmount);
ItemStack extracted = link.extractStack(request, maxRate);
if (extracted != null && !extracted.isEmpty()) {
golem.setCarried(extracted);
golem.startActionTimer();
}
}
// ==================== 辅助方法 ====================
/**
* 获取傀儡 home 位置的容器(对应 TC 的 homeContainer())。
*/
@Nullable
private static Container getHomeContainer(ThaumcraftGolemEntity golem) {
BlockPos home = golem.home();
BlockPos apparatusPos = home.relative(golem.homeFacing().getOpposite());
Container container = containerAt(golem.level(), apparatusPos);
if (container != null) {
return container;
}
return containerAt(golem.level(), home);
}
@Nullable
private static Container containerAt(Level level, BlockPos pos) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof Container container) {
return container;
}
return null;
}
/**
* 计算容器中匹配物品的数量。
* 对应 TC 的 {@code count()} 方法。
*/
private static int countInContainer(Container container, ItemStack match) {
int count = 0;
for (int slot = 0; slot < container.getContainerSize(); slot++) {
ItemStack stack = container.getItem(slot);
if (ItemStack.isSameItemSameComponents(stack, match)) {
count += stack.getCount();
}
}
return count;
}
/**
* 检查容器是否还有空间放入指定物品(对应 TC 的 hasRoom())。
*/
private static boolean hasRoom(Container container, ItemStack stack, Direction side) {
for (int slot = 0; slot < container.getContainerSize(); slot++) {
ItemStack existing = container.getItem(slot);
if (existing.isEmpty()) {
return true;
}
if (ItemStack.isSameItemSameComponents(existing, stack) && existing.getCount() < existing.getMaxStackSize()) {
return true;
}
}
return false;
}
// ==================== 网络同步 ====================
/**
* 每 SYNC_COOLDOWN tick 发送背包连接状态 S2C 包(替代 1.7.10 DataWatcher 同步)。
*/
private static void syncBackpackStatus(ThaumcraftGolemEntity golem, GlobalPos linkTarget) {
int syncCooldown = golem.getPersistentData().getInt("ThEWifiSyncCooldown");
if (syncCooldown > 0) {
golem.getPersistentData().putInt("ThEWifiSyncCooldown", syncCooldown - 1);
return;
}
golem.getPersistentData().putInt("ThEWifiSyncCooldown", SYNC_COOLDOWN);
WirelessAELinkGolem link = new WirelessAELinkGolem(golem, linkTarget);
boolean inRange = link.isConnected();
int status = inRange
? GolemBackpackSyncPacket.STATUS_IN_RANGE
: GolemBackpackSyncPacket.STATUS_OUT_OF_RANGE;
BackpackSkins skin = GolemBackpackHandler.getBackpackSkin(golem);
GolemBackpackSyncPacket packet = new GolemBackpackSyncPacket(
golem.getId(), status, skin.ordinal());
PacketDistributor.sendToPlayersTrackingEntity(golem, packet);
}
// ==================== LIQUID 核心 (ID=5) ====================
/**
* LIQUID 核心 (ID=5): 从 AE 网络提取流体给傀儡(对应 1.7.10 AIGolemWifiLiquid)。
*/
private static void doLiquid(ThaumcraftGolemEntity golem, WirelessAELinkGolem link) {
FluidStack carried = golem.fluidCarried();
if (!carried.isEmpty()) {
return;
}
for (int slot = 0; slot < 9; slot++) {
ItemStack filter = golem.getFilter(slot);
if (filter.isEmpty()) continue;
var fluidHandler = filter.getCapability(net.neoforged.neoforge.capabilities.Capabilities.FluidHandler.ITEM);
if (fluidHandler == null) continue;
for (int tank = 0; tank < fluidHandler.getTanks(); tank++) {
FluidStack fluid = fluidHandler.getFluidInTank(tank);
if (fluid.isEmpty()) continue;
int maxRate = WirelessAELinkGolem.getMaxFluidRate(golem);
int requestAmount = Math.min(golem.fluidCarryLimit(), maxRate);
FluidStack request = fluid.copyWithAmount(requestAmount);
FluidStack extracted = link.extractFluid(request, maxRate);
if (extracted != null && !extracted.isEmpty()) {
golem.setFluidCarried(extracted);
golem.startActionTimer();
return;
}
}
}
}
// ==================== ESSENTIA 核心 (ID=6) ====================
/**
* ESSENTIA 核心 (ID=6): 傀儡与 AE 源质网络交互(对应 1.7.10 AIGolemWifiEssentia)。
*/
private static void doEssentia(ThaumcraftGolemEntity golem, WirelessAELinkGolem link) {
Aspect aspect = golem.essentiaCarried();
int amount = golem.essentiaAmount();
if (aspect != null && amount > 0) {
net.minecraft.resources.ResourceLocation aspectId = TCReflection.getAspectId(aspect);
int deposited = link.depositEssentia(aspectId, amount);
if (deposited > 0) {
int remaining = amount - deposited;
golem.setEssentiaCarried(remaining > 0 ? aspect : null, remaining);
golem.startActionTimer();
}
return;
}
for (int slot = 0; slot < 9; slot++) {
ItemStack filter = golem.getFilter(slot);
if (filter.isEmpty()) continue;
if (filter.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem containerItem) {
AspectList aspectList = containerItem.getAspects(filter);
if (aspectList == null || aspectList.size() == 0) continue;
for (Aspect filterAspect : aspectList.getAspects()) {
net.minecraft.resources.ResourceLocation aspectId = TCReflection.getAspectId(filterAspect);
int requestAmount = Math.min(golem.carryLimit(), WirelessAELinkGolem.getMaxEssentiaRate(golem));
int extracted = link.extractEssentia(aspectId, requestAmount);
if (extracted > 0) {
golem.setEssentiaCarried(filterAspect, extracted);
golem.startActionTimer();
return;
}
}
}
}
}
}
@@ -0,0 +1,312 @@
package thaumicenergistics.common.entities;
import net.minecraft.core.GlobalPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import appeng.api.implementations.blockentities.IWirelessAccessPoint;
import appeng.api.networking.IGrid;
import appeng.api.networking.energy.IEnergyService;
import appeng.api.networking.security.IActionSource;
import appeng.api.stacks.AEFluidKey;
import appeng.api.stacks.AEItemKey;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.blockentity.networking.WirelessAccessPointBlockEntity;
import appeng.util.Platform;
import net.neoforged.neoforge.fluids.FluidStack;
import org.jetbrains.annotations.Nullable;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.common.entities.golem.ThaumcraftGolemEntity;
import java.util.Set;
/**
* 傀儡与 AE2 无线网络的连接桥。
* 提供物品/流体/源质的存取方法,供 {@link GolemBackpackTickHandler} 的 AI 逻辑调用。
*/
public class WirelessAELinkGolem {
private final ThaumcraftGolemEntity golem;
private final GlobalPos linkTarget;
private IGrid cachedGrid;
private IWirelessAccessPoint cachedAccessPoint;
private IActionSource actionSource;
private int tickCounter;
public WirelessAELinkGolem(ThaumcraftGolemEntity golem, GlobalPos linkTarget) {
this.golem = golem;
this.linkTarget = linkTarget;
}
@Nullable
public IGrid getGrid() {
tickCounter++;
if (tickCounter % 20 == 0 || cachedGrid == null) {
updateConnection();
}
return cachedGrid;
}
public boolean isConnected() {
return getGrid() != null;
}
// ==================== 物品网络操作 ====================
/**
* 获取 AE2 网络的物品存储(MEStorage 同时处理物品和流体)。
*/
@Nullable
public MEStorage getItemInventory() {
IGrid grid = getGrid();
if (grid == null) return null;
return grid.getStorageService().getInventory();
}
@Nullable
public IEnergyService getEnergyService() {
IGrid grid = getGrid();
if (grid == null) return null;
return grid.getEnergyService();
}
/**
* 将物品存入 AE 网络。
* 使用 {@link StorageHelper#poweredInsert} 确保有足够能量时才存入。
*
* @param stack 要存入的物品(调用后 count 会被减少)
* @param maxRate 每次操作最大存入数量
*/
public void depositStack(ItemStack stack, int maxRate) {
MEStorage storage = getItemInventory();
if (storage == null) return;
IEnergyService energy = getEnergyService();
if (energy == null) return;
if (actionSource == null) return;
AEItemKey what = AEItemKey.of(stack);
if (what == null) return;
long depositSize = Math.min(stack.getCount(), maxRate);
long inserted = StorageHelper.poweredInsert(energy, storage, what, depositSize, actionSource);
stack.shrink((int) inserted);
}
/**
* 从 AE 网络提取物品。
* 使用 {@link StorageHelper#poweredExtraction} 确保有足够能量时才提取。
*
* @param target 要提取的物品模板(数量为期望数量)
* @param maxRate 每次操作最大提取数量
* @return 提取出的 ItemStack,可能为 null
*/
@Nullable
public ItemStack extractStack(ItemStack target, int maxRate) {
MEStorage storage = getItemInventory();
if (storage == null) return null;
IEnergyService energy = getEnergyService();
if (energy == null) return null;
if (actionSource == null) return null;
AEItemKey what = AEItemKey.of(target);
if (what == null) return null;
long requestSize = Math.min(target.getCount(), maxRate);
long extracted = StorageHelper.poweredExtraction(energy, storage, what, requestSize, actionSource);
if (extracted <= 0) return null;
return what.toStack((int) extracted);
}
// ==================== 流体网络操作 ====================
/**
* 从 AE 网络提取流体。
*
* @param target 要提取的流体模板
* @param maxRateMb 每次操作最大提取量(mB)
* @return 提取出的 FluidStack,可能为 null
*/
@Nullable
public FluidStack extractFluid(FluidStack target, int maxRateMb) {
MEStorage storage = getItemInventory();
if (storage == null) return null;
IEnergyService energy = getEnergyService();
if (energy == null) return null;
if (actionSource == null) return null;
AEFluidKey what = AEFluidKey.of(target);
if (what == null) return null;
long requestSize = Math.min(target.getAmount(), maxRateMb);
long extracted = StorageHelper.poweredExtraction(energy, storage, what, requestSize, actionSource);
if (extracted <= 0) return null;
return what.toStack((int) extracted);
}
/**
* 将流体存入 AE 网络。
*
* @param fluid 流体栈(调用后 amount 会被减少)
* @param maxRateMb 每次操作最大存入量(mB)
*/
public void depositFluid(FluidStack fluid, int maxRateMb) {
MEStorage storage = getItemInventory();
if (storage == null) return;
IEnergyService energy = getEnergyService();
if (energy == null) return;
if (actionSource == null) return;
AEFluidKey what = AEFluidKey.of(fluid);
if (what == null) return;
long depositSize = Math.min(fluid.getAmount(), maxRateMb);
long inserted = StorageHelper.poweredInsert(energy, storage, what, depositSize, actionSource);
fluid.shrink((int) inserted);
}
// ==================== 源质网络操作 ====================
@Nullable
public thaumicenergistics.api.grid.IEssentiaGrid getEssentiaGrid() {
IGrid grid = getGrid();
if (grid == null) return null;
return grid.getService(thaumicenergistics.api.grid.IEssentiaGrid.class);
}
/**
* 从 AE 网络提取源质。
*
* @param aspectId 源质 IDResourceLocation,如 "thaumcraft:ignis"
* @param maxAmount 最大提取量
* @return 实际提取的量
*/
public int extractEssentia(net.minecraft.resources.ResourceLocation aspectId, int maxAmount) {
thaumicenergistics.api.grid.IEssentiaGrid essentiaGrid = getEssentiaGrid();
if (essentiaGrid == null) return 0;
if (actionSource == null) return 0;
int maxRate = getMaxEssentiaRate(golem);
int toExtract = Math.min(maxAmount, maxRate);
return (int) essentiaGrid.extractEssentia(aspectId, toExtract, false);
}
/**
* 将源质存入 AE 网络。
*
* @param aspectId 源质 IDResourceLocation
* @param amount 要存入的量
* @return 实际存入的量
*/
public int depositEssentia(net.minecraft.resources.ResourceLocation aspectId, int amount) {
thaumicenergistics.api.grid.IEssentiaGrid essentiaGrid = getEssentiaGrid();
if (essentiaGrid == null) return 0;
if (actionSource == null) return 0;
int maxRate = getMaxEssentiaRate(golem);
int toDeposit = Math.min(amount, maxRate);
long rejected = essentiaGrid.injectEssentia(aspectId, toDeposit, false);
return toDeposit - (int) rejected;
}
// ==================== 连接管理 ====================
private void updateConnection() {
cachedGrid = null;
cachedAccessPoint = null;
actionSource = null;
Level level = golem.level();
if (!(level instanceof ServerLevel serverLevel)) {
return;
}
var linkedLevel = serverLevel.getServer().getLevel(linkTarget.dimension());
if (linkedLevel == null) {
return;
}
var be = Platform.getTickingBlockEntity(linkedLevel, linkTarget.pos());
if (!(be instanceof IWirelessAccessPoint accessPoint)) {
return;
}
IGrid grid = accessPoint.getGrid();
if (grid == null) {
return;
}
if (!isInRange(serverLevel, grid)) {
return;
}
cachedGrid = grid;
cachedAccessPoint = accessPoint;
// 创建 ActionSource:使用 AP 作为 MachineSource
if (accessPoint instanceof appeng.api.networking.security.IActionHost actionHost) {
actionSource = IActionSource.ofMachine(actionHost);
}
}
private boolean isInRange(ServerLevel level, IGrid grid) {
Set<WirelessAccessPointBlockEntity> accessPoints = grid.getActiveMachines(WirelessAccessPointBlockEntity.class);
if (accessPoints.isEmpty()) {
return false;
}
double golemX = golem.getX();
double golemY = golem.getY();
double golemZ = golem.getZ();
for (WirelessAccessPointBlockEntity ap : accessPoints) {
if (!ap.isActive()) {
continue;
}
var dc = ap.getLocation();
if (dc.getLevel() != level) {
continue;
}
double range = ap.getRange();
double dx = dc.getPos().getX() - golemX;
double dy = dc.getPos().getY() - golemY;
double dz = dc.getPos().getZ() - golemZ;
double sqDist = dx * dx + dy * dy + dz * dz;
if (sqDist <= range * range) {
return true;
}
}
return false;
}
// ==================== 速率计算 ====================
/**
* 物品交互速率(按 1.7.10 AIAENetworkGolem 速率表)。
* 基础: 8/24/32,高级傀儡 ×2
*/
public static int getMaxItemRate(ThaumcraftGolemEntity golem) {
int[] ITEM_RATES = {8, 24, 32};
int rateMult = golem.isAdvanced() ? 2 : 1;
int orderUpgrades = Math.max(0, Math.min(2, golem.getUpgradeAmount(2)));
return ITEM_RATES[orderUpgrades] * rateMult;
}
/**
* 流体交互速率(按 1.7.10 AIAENetworkGolem 速率表)。
* 基础: 100/250/500 mB,高级傀儡 ×2
*/
public static int getMaxFluidRate(ThaumcraftGolemEntity golem) {
int[] FLUID_RATES = {100, 250, 500};
int rateMult = golem.isAdvanced() ? 2 : 1;
int orderUpgrades = Math.max(0, Math.min(2, golem.getUpgradeAmount(2)));
return FLUID_RATES[orderUpgrades] * rateMult;
}
/**
* 源质交互速率(按 1.7.10 AIAENetworkGolem 速率表)。
* 基础: 4/12/16,高级傀儡 ×2
*/
public static int getMaxEssentiaRate(ThaumcraftGolemEntity golem) {
int[] ESS_RATES = {4, 12, 16};
int rateMult = golem.isAdvanced() ? 2 : 1;
int orderUpgrades = Math.max(0, Math.min(2, golem.getUpgradeAmount(2)));
return ESS_RATES[orderUpgrades] * rateMult;
}
}
@@ -0,0 +1,21 @@
package thaumicenergistics.common.features;
/**
* 功能:奥术自动合成。
* 启用奥术装配器及相关合成样板。
* 自 1.7.10 FeatureAutocrafting_Arcane 移植(约 7k 行)。
*/
public class FeatureAutocraftingArcane extends ThEFeatureBase {
public FeatureAutocraftingArcane() { super("arcane_crafting_terminal"); }
@Override
protected void doRegister() {
// Arcane Assembler 方块经 ModBlocks 注册
// TODO: Register arcane crafting pattern encoder, crafting CPU integration
}
@Override
protected void doPostInit() {
// TODO: Register arcane crafting recipes, research entries
}
}
@@ -0,0 +1,20 @@
package thaumicenergistics.common.features;
/**
* 源质存储元件功能。
* 注册 cell 物品、元件工作台与存储组件物品。
* 自 1.7.10 FeatureCells 移植(约 12k 行)。
*/
public class FeatureCells extends ThEFeatureBase {
public FeatureCells() { super("cells"); }
@Override
protected void doRegister() {
// 物品经 init 包的 ModItems 注册——本功能校验其存在
}
@Override
protected void doPostInit() {
// TODO: Register cell recipes, research entries, cell workbench functionality
}
}
@@ -0,0 +1,35 @@
package thaumicenergistics.common.features;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.init.ModItems;
/**
* 功能:源质 IO 总线。
* 启用源质输入/输出/存储总线Part部件
* Part注册已移至模组主类ThaumicEnergistics构造方法中,
* 通过AE2官方PartHelper.registerPart() API完成,
* 本Feature负责功能开关、配方注册、研究数据等跨模组集成内容。
*/
public class FeatureEssentiaIOBuses extends ThEFeatureBase {
public FeatureEssentiaIOBuses() { super("io_buses"); }
@Override
protected void doRegister() {
// ===== Part核心注册已在ThaumicEnergistics构造方法中完成 =====
// 原因:AE2 PartHelper.registerPart()需要DeferredItem注册完成后立即调用,
// Feature.register()的调用时机同样满足,但为了集中管理Part注册入口,统一在主类中处理。
// ===== 以下登记本Feature涉及的物品,供跨模组引用追踪 =====
ThaumicEnergistics.LOG.info(" -> EssentiaImportBus PartItem: {} (ready)",
ModItems.ESSENTIA_IMPORT_BUS.getId());
ThaumicEnergistics.LOG.info(" -> EssentiaExportBus PartItem: {} (pending)",
ModItems.ESSENTIA_EXPORT_BUS.getId());
ThaumicEnergistics.LOG.info(" -> EssentiaStorageBus PartItem: {} (pending)",
ModItems.ESSENTIA_STORAGE_BUS.getId());
}
@Override
protected void doPostInit() {
// 后置初始化:注册神秘时代研究条目、AE2合成配方等
}
}
@@ -0,0 +1,15 @@
package thaumicenergistics.common.features;
/**
* 源质监控功能。
* 启用源质水平检测器、存储监控器与转换监控器部件。
* 自 1.7.10 FeatureEssentiaMonitoring 移植(约 6k 行)。
*/
public class FeatureEssentiaMonitoring extends ThEFeatureBase {
public FeatureEssentiaMonitoring() { super("monitoring"); }
@Override
protected void doRegister() {
// TODO: Register monitoring-related parts when AE2 part API is integrated
}
}
@@ -0,0 +1,7 @@
package thaumicenergistics.common.features;
/** 功能:源质供应器。从 AE 网络核心抽取源质。 */
public class FeatureEssentiaProvider extends ThEFeatureBase {
public FeatureEssentiaProvider() { super("essentia_provider"); }
@Override protected void doRegister() {}
}
@@ -0,0 +1,21 @@
package thaumicenergistics.common.features;
/**
* 功能:源质谐振仓。
* 启用基于源质的能量生产。
* 自 1.7.10 FeatureEssentiaVibrationChamber 移植(约 4k 行)。
*/
public class FeatureEssentiaVibrationChamber extends ThEFeatureBase {
public FeatureEssentiaVibrationChamber() { super("vibration_chamber"); }
@Override
protected void doRegister() {
// EVC 方块经 ModBlocks 注册
// TODO: Register fuel values for each essentia type
}
@Override
protected void doPostInit() {
// TODO: Register EVC recipe, research entry
}
}
@@ -0,0 +1,11 @@
package thaumicenergistics.common.features;
/** 功能:无线傀儡背包。启用傀儡访问 AE 网络。 */
public class FeatureGolemBackpack extends ThEFeatureBase {
public FeatureGolemBackpack() { super("golem_backpack"); }
@Override
protected void doRegister() {
// GridLinkables 注册在 commonSetup 阶段完成(需要物品已绑定)
}
}
@@ -0,0 +1,16 @@
package thaumicenergistics.common.features;
/**
* 功能:注魔供应器。
* 为 TC 注魔自动供应源质。
* 自 1.7.10 FeatureInfusionProvider 移植(约 4k 行)。
*/
public class FeatureInfusionProvider extends ThEFeatureBase {
public FeatureInfusionProvider() { super("infusion_provider"); }
@Override
protected void doRegister() {
// Infusion Provider 方块经 ModBlocks 注册
// TODO: Register infusion detection and essentia delivery logic
}
}
@@ -0,0 +1,17 @@
package thaumicenergistics.common.features;
import thaumicenergistics.ThaumicEnergistics;
public final class FeatureRegistry {
private FeatureRegistry() {}
private static final ThEFeatureBase[] FEATURES = {
new FeatureCells(), new FeatureEssentiaIOBuses(), new FeatureEssentiaMonitoring(),
new FeatureEssentiaVibrationChamber(), new FeatureInfusionProvider(),
new FeatureAutocraftingArcane(), new FeatureVisRelayInterface(),
new FeatureGolemBackpack(), new FeatureEssentiaProvider()
};
public static void registerAll() {
for (var f : FEATURES) { ThaumicEnergistics.LOG.info("Feature [{}] {}", f.getName(), f.isEnabled() ? "enabled":"disabled"); f.register(); }
}
public static void postInitAll() { for (var f : FEATURES) f.postInit(); }
}
@@ -0,0 +1,17 @@
package thaumicenergistics.common.features;
import appeng.api.features.P2PTunnelAttunement;
import thaumicenergistics.init.ModItems;
public class FeatureVisRelayInterface extends ThEFeatureBase {
public FeatureVisRelayInterface() { super("vis_interface"); }
@Override
protected void doRegister() {
P2PTunnelAttunement.registerAttunementTag(ModItems.VIS_INTERFACE.get());
}
@Override
protected void doPostInit() {
}
}
@@ -0,0 +1,379 @@
package thaumicenergistics.common.features;
import net.minecraft.world.item.crafting.Ingredient;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.tags.TagKey;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Blocks;
import thaumcraft.api.ThaumcraftApi;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.CrucibleRecipe;
import thaumcraft.api.crafting.InfusionRecipe;
import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumcraft.common.vis.TCVisHelper;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.init.ModBlocks;
import thaumicenergistics.init.ModItems;
public final class RecipeRegistration {
private RecipeRegistration() {}
// ==================== 配方引用(供研究页面使用) ====================
// 注魔配方
public static InfusionRecipe ESSENTIA_PROVIDER;
public static InfusionRecipe INFUSION_PROVIDER;
public static InfusionRecipe ARCANE_ASSEMBLER;
// 奥术有序配方
public static ShapedArcaneRecipe STORAGE_COMPONENT_1K;
public static ShapedArcaneRecipe STORAGE_COMPONENT_4K;
public static ShapedArcaneRecipe STORAGE_COMPONENT_16K;
public static ShapedArcaneRecipe STORAGE_COMPONENT_64K;
public static ShapedArcaneRecipe KNOWLEDGE_CORE_ITEM;
public static ShapedArcaneRecipe FOCUS_AEWRENCH_RECIPE;
public static ShapedArcaneRecipe GOLEM_WIFI_BACKPACK_RECIPE;
public static ShapedArcaneRecipe ESSENTIA_IMPORT_BUS;
public static ShapedArcaneRecipe ESSENTIA_EXPORT_BUS;
public static ShapedArcaneRecipe ESSENTIA_STORAGE_BUS;
public static ShapedArcaneRecipe KNOWLEDGE_INSCRIBER_BLOCK;
public static ShapedArcaneRecipe DISTILLATION_ENCODER_BLOCK;
// 奥术无序配方
public static ShapelessArcaneRecipe DIFFUSION_CORE;
public static ShapelessArcaneRecipe COALESCENCE_CORE;
public static ShapelessArcaneRecipe ESSENTIA_LEVEL_EMITTER;
public static ShapelessArcaneRecipe ESSENTIA_TERMINAL;
public static ShapelessArcaneRecipe ARCANE_CRAFTING_TERMINAL;
public static ShapelessArcaneRecipe VIS_INTERFACE;
public static ShapelessArcaneRecipe ESSENTIA_VIBRATION_CHAMBER;
private static final TagKey<Item> WISP_ESSENCES = tag("thaumicenergistics", "wisp_essences");
@SuppressWarnings("SameParameterValue")
private static TagKey<Item> tag(String ns, String path) {
return TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath(ns, path));
}
public static void registerAll() {
registerShapedArcane();
registerShapelessArcane();
registerInfusion();
ThaumicEnergistics.LOG.info("ThaumicEnergistics recipes registered.");
}
private static void registerShapedArcane() {
STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE1K", stack(ModItems.STORAGE_COMPONENT_1K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1),
"EQ ",
"QPQ",
" QE",
'E', WISP_ESSENCES,
'Q', ae("certus_quartz_crystal"),
'P', ae("logic_processor"));
STORAGE_COMPONENT_4K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE4K", stack(ModItems.STORAGE_COMPONENT_4K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 2),
"EPE",
"1G1",
"E1E",
'E', WISP_ESSENCES,
'P', ae("calculation_processor"),
'1', stack(ModItems.STORAGE_COMPONENT_1K),
'G', ae("quartz_glass"));
STORAGE_COMPONENT_16K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE16K", stack(ModItems.STORAGE_COMPONENT_16K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 4),
"SPE",
"4G4",
"E4S",
'S', tc("salis_mundus"),
'P', ae("engineering_processor"),
'4', stack(ModItems.STORAGE_COMPONENT_4K),
'G', ae("quartz_glass"),
'E', WISP_ESSENCES);
STORAGE_COMPONENT_64K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE64K", stack(ModItems.STORAGE_COMPONENT_64K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 8),
"SPS",
"6G6",
"S6S",
'S', tc("salis_mundus"),
'P', ae("engineering_processor"),
'6', stack(ModItems.STORAGE_COMPONENT_16K),
'G', ae("quartz_glass"));
KNOWLEDGE_CORE_ITEM = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModItems.KNOWLEDGE_CORE),
new AspectList().add(Aspect.WATER, 3).add(Aspect.ORDER, 3).add(Aspect.EARTH, 1),
"QlQ",
"lBl",
"QPQ",
'Q', ae("quartz_glass"),
'l', new ItemStack(Items.LAPIS_LAZULI, 4),
'B', tc("zombie_brain"),
'P', ae("calculation_processor"));
FOCUS_AEWRENCH_RECIPE = ThaumcraftApi.addArcaneCraftingRecipe("FOCUS_AEWRENCH", stack(ModItems.FOCUS_AEWRENCH),
new AspectList().add(Aspect.AIR, 10).add(Aspect.FIRE, 10),
"AqF",
"qWq",
"FqA",
'A', tc("air_shard"),
'q', new ItemStack(Items.QUARTZ, 4),
'F', tc("fire_shard"),
'W', ae("certus_quartz_wrench"));
GOLEM_WIFI_BACKPACK_RECIPE = ThaumcraftApi.addArcaneCraftingRecipe("GOLEMWIFI", stack(ModItems.GOLEM_WIFI_BACKPACK),
new AspectList().add(Aspect.AIR, 6).add(Aspect.FIRE, 4).add(Aspect.ORDER, 3),
"tIt",
"nRn",
"fCf",
't', tc("thaumium_ingot"),
'I', ae("interface"),
'n', tc("nitor"),
'R', ae("wireless_receiver"),
'f', ae("fluix_crystal"),
'C', ae("charger"));
ESSENTIA_IMPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_IMPORT_BUS),
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
"JDJ",
"IFI",
" ",
'J', tc("warded_jar"),
'D', stack(ModItems.DIFFUSION_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'F', tc("essentia_filter"));
ESSENTIA_EXPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_EXPORT_BUS),
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
"JCJ",
"IFI",
" ",
'J', tc("warded_jar"),
'C', stack(ModItems.COALESCENCE_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'F', tc("essentia_filter"));
ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS),
new AspectList().add(Aspect.EARTH, 3).add(Aspect.FIRE, 3).add(Aspect.WATER, 3),
"DCF",
"IGI",
" ",
'D', stack(ModItems.DIFFUSION_CORE),
'C', stack(ModItems.COALESCENCE_CORE),
'F', tc("essentia_filter"),
'I', new ItemStack(Items.IRON_INGOT, 2),
'G', ae("quartz_glass"));
KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
new AspectList().add(Aspect.WATER, 5).add(Aspect.EARTH, 5).add(Aspect.FIRE, 5).add(Aspect.AIR, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("monitor"),
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
new AspectList().add(Aspect.WATER, 5).add(Aspect.EARTH, 5).add(Aspect.FIRE, 5).add(Aspect.AIR, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("dark_monitor"),
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
new AspectList().add(Aspect.WATER, 5).add(Aspect.EARTH, 5).add(Aspect.FIRE, 5).add(Aspect.AIR, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("semi_dark_monitor"),
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
DISTILLATION_ENCODER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("monitor"),
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("dark_monitor"),
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("semi_dark_monitor"),
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
}
private static void registerShapelessArcane() {
// 1. 扩散核心
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
new AspectList().add(Aspect.WATER, 2).add(Aspect.ENTROPY, 2),
tc("quicksilver"),
tc("quicksilver"),
tc("quicksilver"),
tc("entropy_shard"),
ae("annihilation_core"));
// 2. 凝聚核心
COALESCENCE_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.COALESCENCE_CORE),
new AspectList().add(Aspect.WATER, 2).add(Aspect.ORDER, 2),
tc("quicksilver"),
tc("quicksilver"),
tc("quicksilver"),
tc("order_shard"),
ae("formation_core"));
// 3. 原质等级发射器
ESSENTIA_LEVEL_EMITTER = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAMONITORING", new ItemStack(ModItems.ESSENTIA_LEVEL_EMITTER.get(), 1),
new AspectList().add(Aspect.FIRE, 4),
ae("calculation_processor"),
new ItemStack(Items.REDSTONE_TORCH),
tc("salis_mundus"));
// 4. 原质终端
ESSENTIA_TERMINAL = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一
ae("monitor"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"),
tc("vis_filter"));
ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一
ae("dark_monitor"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"),
tc("vis_filter"));
ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一
ae("semi_dark_monitor"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"),
tc("vis_filter"));
// 5. 奥术合成终端
ARCANE_CRAFTING_TERMINAL = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ARCANECRAFTINGTERMINAL", stack(ModItems.ARCANE_CRAFTING_TERMINAL_ITEM),
new AspectList().add(Aspect.AIR, 10).add(Aspect.EARTH, 10).add(Aspect.FIRE, 10).add(Aspect.WATER, 10).add(Aspect.ORDER, 10).add(Aspect.ENTROPY, 10),
ae("terminal"),
tc("arcane_workbench"),
ae("calculation_processor"));
// 6. 源流接口
VIS_INTERFACE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("VISINTERFACE", stack(ModItems.VIS_INTERFACE),
new AspectList().add(Aspect.AIR, 2).add(Aspect.EARTH, 2).add(Aspect.FIRE, 2).add(Aspect.WATER, 2).add(Aspect.ORDER, 2).add(Aspect.ENTROPY, 2),
tc("balance_shard"),
ae("me_p2p_tunnel"));
// 7. 原质振荡室
ESSENTIA_VIBRATION_CHAMBER = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAVIBRATIONCHAMBER", stack(ModBlocks.ESSENTIA_VIBRATION_CHAMBER),
new AspectList().add(Aspect.FIRE, 10).add(Aspect.WATER, 6).add(Aspect.ORDER, 4).add(Aspect.ENTROPY, 4),
ae("vibration_chamber"),
tc("jar_of_essentia"),
stack(ModItems.COALESCENCE_CORE));
}
private static void registerInfusion() {
// 1. 原质供应器
ESSENTIA_PROVIDER = ThaumcraftApi.addInfusionCraftingRecipe("ESSENTIAPROVIDER", stack(ModBlocks.ESSENTIA_PROVIDER),
1, new AspectList().add(Aspect.MECHANISM, 64).add(Aspect.MAGIC, 32).add(Aspect.ORDER, 32).add(Aspect.EXCHANGE, 16),
ae("interface"),
tc("water_shard"),
tc("water_shard"),
tc("salis_mundus"),
tc("salis_mundus"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
tc("essentia_filter"),
tc("essentia_filter"));
// 2. 注魔供应器
INFUSION_PROVIDER = ThaumcraftApi.addInfusionCraftingRecipe("INFUSIONPROVIDER", stack(ModBlocks.INFUSION_PROVIDER),
3, new AspectList().add(Aspect.MECHANISM, 64).add(Aspect.MAGIC, 32).add(Aspect.ORDER, 32).add(Aspect.EXCHANGE, 16),
ae("interface"),
tc("essentia_mirror"),
tc("essentia_mirror"),
tc("air_shard"),
tc("air_shard"),
stack(ModItems.COALESCENCE_CORE),
stack(ModItems.COALESCENCE_CORE),
tc("salis_mundus"),
tc("salis_mundus"));
// 3. 奥术装配器
ARCANE_ASSEMBLER = ThaumcraftApi.addInfusionCraftingRecipe("ARCANEASSEMBLER", stack(ModBlocks.ARCANE_ASSEMBLER),
5, new AspectList().add(Aspect.CRAFT, 64).add(Aspect.EXCHANGE, 32).add(Aspect.AURA, 16).add(Aspect.MAGIC, 16).add(Aspect.METAL, 8).add(Aspect.CRYSTAL, 8),
ae("molecular_assembler"),
tc("fire_shard"),
tc("entropy_shard"),
tc("order_shard"),
tc("balance_shard"),
tc("water_shard"),
tc("earth_shard"),
tc("air_shard"),
tc("salis_mundus"),
createFullVisScepter());
}
private static ItemStack createFullVisScepter() {
ItemStack scepter = tc("silverwood_thaumium_scepter");
if (scepter.isEmpty()) return scepter;
TCVisHelper.setWandParts(scepter, "silverwood", "thaumium", true);
int max = TCVisHelper.getWandMaxVis(scepter, "silverwood_thaumium_scepter");
for (Aspect a : Aspect.getPrimalAspects()) {
TCVisHelper.storeVis(scepter, a, max, max);
}
return scepter;
}
private static ItemStack stack(java.util.function.Supplier<? extends net.minecraft.world.level.ItemLike> s) {
return new ItemStack(s.get());
}
private static ItemStack tc(String name) {
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", name)));
}
private static ItemStack ae(String name) {
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("ae2", name)));
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.features;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.config.ThEConfig;
import java.util.function.Supplier;
/**
* 所有功能模块的基类。每个功能可独立启停。
* 自 1.7.10 ThEFeatureBase 移植。
*/
public abstract class ThEFeatureBase {
private final String name;
protected boolean enabled;
protected ThEFeatureBase(String name) {
this.name = name;
this.enabled = ThEConfig.isFeatureEnabled(name);
}
public final String getName() { return name; }
public final boolean isEnabled() { return enabled; }
/** 模组构造期间调用,用于注册物品与方块。 */
public void register() {
if (!enabled) return;
ThaumicEnergistics.LOG.info("Feature [{}] loading...", name);
doRegister();
}
/** 所有功能注册完成后调用,用于跨功能设置。 */
public void postInit() {
if (!enabled) return;
doPostInit();
}
/** 覆写以注册本功能特有的方块/物品。 */
protected abstract void doRegister();
/** 覆写以进行注册后设置(配方、研究等)。 */
protected void doPostInit() {}
}
@@ -0,0 +1,22 @@
package thaumicenergistics.common.grid;
/**
* 表示连接到 AE2 网格的部件。
* 对应 1.7.10 AEPartGridBlock——为 1.21.1 简化。
* TODO: 通过 AE2 19.x API 完整连接 IGridNode。
*/
public class AEPartGridBlock {
private final Object part; // 占位 PartBase 或 IPartItem
public AEPartGridBlock(Object part) { this.part = part; }
public double getIdlePowerUsage() {
// TODO: Delegate to part
return 1.0;
}
public Object getGridNode() {
// TODO: AE2 IGridNode
return null;
}
}
@@ -0,0 +1,136 @@
package thaumicenergistics.common.grid;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import net.minecraft.resources.ResourceLocation;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridServiceProvider;
import appeng.api.networking.security.IActionSource;
import appeng.api.networking.storage.IStorageService;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.api.networking.security.IActionSource;
import thaumicenergistics.api.grid.IEssentiaGrid;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.api.storage.IAspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/**
* 源质网格服务的实现。查询和提取源质时同时检查两个存储后端:
* Path A: EssentiaMonitor(注魔供应器/傀儡背包直接写入),
* Path B: AE2 原生存储(源质存储元件、输入总线等)。
*/
public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider {
private final EssentiaMonitor monitor;
private final IGrid grid;
public EssentiaGridService(IGrid grid) {
this.monitor = EssentiaMonitorRegistry.getMonitor(grid);
this.grid = grid;
}
private long getNativeAmount(ResourceLocation aspectId) {
var storage = getNativeStorage();
if (storage == null) return 0;
var key = AEssentiaKey.of(aspectId);
if (key == null) return 0;
for (var entry : storage.getAvailableStacks()) {
if (entry.getKey() instanceof AEssentiaKey ek && ek.equals(key)) {
return entry.getLongValue();
}
}
return 0;
}
private long extractNative(ResourceLocation aspectId, long amount, boolean simulate) {
var storage = getNativeStorage();
if (storage == null) return 0;
var key = AEssentiaKey.of(aspectId);
if (key == null) return 0;
return StorageHelper.poweredExtraction(
grid.getEnergyService(), storage, key, amount,
IActionSource.empty());
}
private MEStorage getNativeStorage() {
var sg = grid.getService(IStorageService.class);
return sg != null ? sg.getInventory() : null;
}
@Override
public long getEssentiaAmount(ResourceLocation aspectId) {
long monitorAmt = monitor.get(aspectId);
long nativeAmt = getNativeAmount(aspectId);
return monitorAmt + nativeAmt;
}
@Override
public long extractEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
// 优先从 monitor 提取
long monitorAmt = monitor.get(aspectId);
if (monitorAmt >= amount) {
if (!simulate) {
monitor.set(aspectId, monitorAmt - amount);
}
return amount;
}
// monitor 不够,全部取出后再从 AE2 原生存储提取剩余
long extracted = 0;
if (monitorAmt > 0) {
if (!simulate) {
monitor.set(aspectId, 0);
}
extracted = monitorAmt;
}
long remaining = amount - extracted;
if (remaining > 0) {
long nativeExtracted = extractNative(aspectId, remaining, simulate);
extracted += nativeExtracted;
}
return extracted;
}
@Override
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
if (amount <= 0) return 0;
if (!simulate) {
long current = monitor.get(aspectId);
monitor.set(aspectId, current + amount);
}
return 0;
}
@Override
public List<IAspectStack> getEssentiaList() {
Map<ResourceLocation, Long> merged = new LinkedHashMap<>();
for (var stack : monitor.list()) {
merged.put(stack.aspectId(), stack.amount());
}
var storage = getNativeStorage();
if (storage != null) {
for (var entry : storage.getAvailableStacks()) {
if (entry.getKey() instanceof AEssentiaKey ek) {
merged.merge(ek.getId(), entry.getLongValue(), Long::sum);
}
}
}
List<IAspectStack> result = new ArrayList<>();
for (var entry : merged.entrySet()) {
if (entry.getValue() > 0) {
result.add(new AspectStack(entry.getKey(), entry.getValue()));
}
}
return result;
}
}
@@ -0,0 +1,15 @@
package thaumicenergistics.common.grid;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import net.minecraft.resources.ResourceLocation;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.api.storage.IAspectStack;
public class EssentiaMonitor {
private final Map<ResourceLocation, Long> storage = Collections.synchronizedMap(new LinkedHashMap<>());
private final List<Runnable> listeners = new CopyOnWriteArrayList<>();
public List<IAspectStack> list() { synchronized(storage) { return storage.entrySet().stream().map(e -> (IAspectStack)new AspectStack(e.getKey(), e.getValue())).toList(); } }
public long get(ResourceLocation id) { synchronized(storage) { return storage.getOrDefault(id, 0L); } }
public void addListener(Runnable r) { listeners.add(r); }
public void set(ResourceLocation id, long amt) { synchronized(storage) { storage.put(id, amt); } listeners.forEach(Runnable::run); }
}
@@ -0,0 +1,29 @@
package thaumicenergistics.common.grid;
import appeng.api.networking.IGrid;
import java.util.Map;
import java.util.WeakHashMap;
/**
* 管理 AE2 Grid → EssentiaMonitor 的映射。
* 每个 AE2 网格对应一个独立的 EssentiaMonitor 实例,
* 确保不同网络的源质存储互不干扰。
* 使用 WeakHashMap,当 Grid 被 GC 时自动清理。
*/
public final class EssentiaMonitorRegistry {
private EssentiaMonitorRegistry() {}
private static final Map<IGrid, EssentiaMonitor> MONITORS = new WeakHashMap<>();
/**
* 获取指定 Grid 对应的 EssentiaMonitor。
* 如果不存在则创建一个新的。
*/
public static EssentiaMonitor getMonitor(IGrid grid) {
synchronized (MONITORS) {
return MONITORS.computeIfAbsent(grid, g -> new EssentiaMonitor());
}
}
}
@@ -0,0 +1,48 @@
package thaumicenergistics.common.grid;
import net.minecraft.resources.ResourceLocation;
import thaumicenergistics.api.grid.IEssentiaWatcher;
import thaumicenergistics.api.grid.IEssentiaWatcherHost;
import java.util.*;
/**
* 监听源质网格的变化并通知 host。
*/
public class EssentiaWatcher implements IEssentiaWatcher {
private final IEssentiaWatcherHost host;
private final Set<ResourceLocation> watchedAspects = new HashSet<>();
private final Map<ResourceLocation, Long> lastAmounts = new HashMap<>();
private final EssentiaMonitor monitor;
public EssentiaWatcher(IEssentiaWatcherHost host, EssentiaMonitor monitor) {
this.host = host;
this.monitor = monitor;
}
@Override public IEssentiaWatcherHost getHost() { return host; }
@Override public int size() { return watchedAspects.size(); }
@Override public boolean isEmpty() { return watchedAspects.isEmpty(); }
@Override public boolean contains(Object o) { return watchedAspects.contains(o); }
@Override public Iterator<ResourceLocation> iterator() { return watchedAspects.iterator(); }
@Override public Object[] toArray() { return watchedAspects.toArray(); }
@Override public <T> T[] toArray(T[] a) { return watchedAspects.toArray(a); }
@Override public boolean add(ResourceLocation id) { return watchedAspects.add(id); }
@Override public boolean remove(Object o) { lastAmounts.remove(o); return watchedAspects.remove(o); }
@Override public boolean containsAll(Collection<?> c) { return watchedAspects.containsAll(c); }
@Override public boolean addAll(Collection<? extends ResourceLocation> c) { return watchedAspects.addAll(c); }
@Override public boolean removeAll(Collection<?> c) { lastAmounts.keySet().removeAll(c); return watchedAspects.removeAll(c); }
@Override public boolean retainAll(Collection<?> c) { lastAmounts.keySet().retainAll(c); return watchedAspects.retainAll(c); }
@Override public void clear() { lastAmounts.clear(); watchedAspects.clear(); }
/** 检查所有被监听的源质,并在变化时通知 host。 */
public void tick() {
for (ResourceLocation id : watchedAspects) {
long current = monitor.get(id);
Long last = lastAmounts.get(id);
if (last == null || last != current) {
lastAmounts.put(id, current);
host.onEssentiaChange(id, current, 0);
}
}
}
}
@@ -0,0 +1,9 @@
package thaumicenergistics.common.grid;
import java.util.*;
import net.minecraft.resources.ResourceLocation;
public class GridEssentiaCache {
private final Map<ResourceLocation, Long> data = Collections.synchronizedMap(new LinkedHashMap<>());
public long get(ResourceLocation id) { return data.getOrDefault(id, 0L); }
public void put(ResourceLocation id, long amt) { data.put(id, amt); }
public Map<ResourceLocation, Long> snapshot() { synchronized(data) { return new LinkedHashMap<>(data); } }
}
@@ -0,0 +1,135 @@
package thaumicenergistics.common.integration.appeng;
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import net.minecraft.core.BlockPos;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.AEKeyType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class AEssentiaKey extends AEKey {
/** id → key 实例的缓存;AEssentiaKey 不可变,且 equals/hashCode 基于 id。 */
private static final Map<ResourceLocation, AEssentiaKey> CACHE = new ConcurrentHashMap<>();
/**
* 规范化 id 实例:所有同 id 的 AEssentiaKey 共享同一 ResourceLocation 实例。
* AE2 的 KeyCounter 内部按 {@code getPrimaryKey()} 的引用相等分组(Reference2ObjectMap),
* 存储(NBT 反序列化)与 configTCReflection 生成)的 id 若不共享实例会导致 AE2 tooltip 去重失败(同方面重复显示)。
*/
private static final Map<ResourceLocation, ResourceLocation> ID_CACHE = new ConcurrentHashMap<>();
public static final MapCodec<AEssentiaKey> MAP_CODEC = RecordCodecBuilder.mapCodec(
builder -> builder.group(
ResourceLocation.CODEC.fieldOf("id").forGetter(AEssentiaKey::getId)
).apply(builder, AEssentiaKey::new)
);
public static final Codec<AEssentiaKey> CODEC = MAP_CODEC.codec();
public static final StreamCodec<RegistryFriendlyByteBuf, AEssentiaKey> STREAM_CODEC =
StreamCodec.composite(
ResourceLocation.STREAM_CODEC, AEssentiaKey::getId,
AEssentiaKey::new
);
private final ResourceLocation id;
@Override
protected Component computeDisplayName() {
// 使用 TC 源质的实际显示名称(如 "Aer" / "风"),而非翻译键
String tag = id.getPath();
if (thaumcraft.api.aspects.Aspect.aspects != null) {
for (thaumcraft.api.aspects.Aspect aspect : thaumcraft.api.aspects.Aspect.aspects.values()) {
if (aspect != null && aspect.getTag() != null && aspect.getTag().equalsIgnoreCase(tag)) {
return aspect.displayName();
}
}
}
// 回退:翻译键
return Component.translatable("aspect.thaumicenergistics." + tag);
}
public void addDrops(long amount, List<ItemStack> drops, Level level, BlockPos pos) {
}
@Override
public boolean hasComponents() {
return false;
}
public AEssentiaKey(ResourceLocation id) {
this.id = ID_CACHE.computeIfAbsent(id, k -> k);
}
@Override
public AEKeyType getType() {
return AEssentiaKeyType.INSTANCE;
}
@Override
public AEKey dropSecondary() {
return this;
}
@Override
public CompoundTag toTag(HolderLookup.Provider registries) {
var ops = registries.createSerializationContext(NbtOps.INSTANCE);
return (CompoundTag) CODEC.encodeStart(ops, this).getOrThrow();
}
@Override
public ResourceLocation getId() {
return id;
}
@Override
public void writeToPacket(RegistryFriendlyByteBuf data) {
ResourceLocation.STREAM_CODEC.encode(data, id);
}
public static AEssentiaKey fromPacket(RegistryFriendlyByteBuf data) {
return new AEssentiaKey(ResourceLocation.STREAM_CODEC.decode(data));
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AEssentiaKey that)) return false;
return id.equals(that.id);
}
@Override
public int hashCode() {
return id.hashCode();
}
@Override
public String toString() {
return "AEssentiaKey{" + id + "}";
}
@Override
public Object getPrimaryKey(){
return id;
}
public static AEssentiaKey of(ResourceLocation id) {
return CACHE.computeIfAbsent(id, AEssentiaKey::new);
}
}
@@ -0,0 +1,54 @@
package thaumicenergistics.common.integration.appeng;
import com.mojang.serialization.MapCodec;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.resources.ResourceLocation;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.AEKeyType;
import thaumicenergistics.ThaumicEnergistics;
public final class AEssentiaKeyType extends AEKeyType {
public static final ResourceLocation ID = ThaumicEnergistics.id("essentia");
public static final AEssentiaKeyType INSTANCE = new AEssentiaKeyType();
private AEssentiaKeyType() {
super(ID, AEssentiaKey.class, Component.translatable("ae2.keytype.thaumicenergistics.essentia"));
}
@Override
public MapCodec<? extends AEKey> codec() {
return AEssentiaKey.MAP_CODEC;
}
public StreamCodec<RegistryFriendlyByteBuf, ? extends AEKey> streamCodec() {
return AEssentiaKey.STREAM_CODEC;
}
@Override
public AEKey readFromPacket(RegistryFriendlyByteBuf input) {
return AEssentiaKey.fromPacket(input);
}
public String formatAmount(long amount, boolean large) {
if (large) {
if (amount >= 1000000) return String.format("%.1fM", amount / 1000000.0);
if (amount >= 1000) return String.format("%.1fK", amount / 1000.0);
}
return Long.toString(amount);
}
@Override
public int getAmountPerByte() {
return 1;
}
@Override
public int getAmountPerUnit() {
return 1;
}
}
@@ -0,0 +1,31 @@
package thaumicenergistics.common.integration.appeng;
import net.minecraft.resources.ResourceLocation;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.api.storage.IAspectStack;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
public final class EssentiaStorageChannel {
private EssentiaStorageChannel() {}
public static GenericStack toGenericStack(IAspectStack stack) {
if (stack == null || stack.isEmpty()) return null;
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
return new GenericStack(key, stack.amount());
}
public static IAspectStack fromGenericStack(Object obj) {
if (obj instanceof GenericStack gs) {
AEKey what = gs.what();
if (what instanceof AEssentiaKey essKey) {
return new AspectStack(essKey.getId(), gs.amount());
}
}
return AspectStack.EMPTY;
}
public static AEssentiaKey toKey(ResourceLocation aspectId) {
return AEssentiaKey.of(aspectId);
}
}
@@ -0,0 +1,7 @@
package thaumicenergistics.common.integration.appeng;
import org.slf4j.Logger; import org.slf4j.LoggerFactory;
public final class ThEAppliedEnergistics {
private static final Logger LOG = LoggerFactory.getLogger("ThE-AE2");
private ThEAppliedEnergistics() {}
public static void init() { LOG.info("ThaumicEnergistics AE2 integration stub loaded."); }
}
@@ -0,0 +1,24 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.world.level.block.Block;
import snownee.jade.api.IWailaClientRegistration;
import snownee.jade.api.IWailaCommonRegistration;
import snownee.jade.api.IWailaPlugin;
import snownee.jade.api.WailaPlugin;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.init.ModBlocks;
@WailaPlugin
public class ThEJadePlugin implements IWailaPlugin {
@Override
public void register(IWailaCommonRegistration registration) {
registration.registerBlockDataProvider(VibrationChamberProvider.INSTANCE, TileEssentiaVibrationChamber.class);
}
@Override
public void registerClient(IWailaClientRegistration registration) {
Block chamberBlock = ModBlocks.ESSENTIA_VIBRATION_CHAMBER.get();
registration.registerBlockComponent(VibrationChamberProvider.INSTANCE, chamberBlock.getClass());
}
}
@@ -0,0 +1,123 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import snownee.jade.api.ui.BoxStyle;
import snownee.jade.api.ui.IElementHelper;
import appeng.util.Platform;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
public enum VibrationChamberProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final ResourceLocation UID = ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "vibration_chamber");
private static final String TAG_STORED = "StoredEssentia";
private static final String TAG_MAX = "MaxEssentia";
private static final String TAG_BURN_TICKS = "BurnTicks";
private static final String TAG_TOTAL_BURN = "TotalBurnTicks";
private static final String TAG_ASPECT = "AspectTag";
private static final String TAG_AE_PER_TICK = "AEPerTick";
private static final String TAG_ENERGY_STORED = "StoredEnergy";
private static final String TAG_ENERGY_MAX = "MaxEnergy";
private static final String TAG_ENERGY_OUTPUT = "MaxOutput";
private static final String TAG_GENERATING = "Generating";
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
CompoundTag data = accessor.getServerData();
if (data == null || data.isEmpty()) return;
// 吸力类型
if (data.contains(TAG_ASPECT)) {
String aspect = data.getString(TAG_ASPECT);
String aspectKey = "thaumicenergistics.aspect." + aspect;
tooltip.add(Component.translatable("thaumicenergistics.jade.suction_type",
Component.translatable(aspectKey)));
}
// 源质存储量
int stored = data.getInt(TAG_STORED);
int max = data.getInt(TAG_MAX);
tooltip.add(Component.translatable("thaumicenergistics.jade.essentia_stored", stored, max));
// 能量槽(ME 控制器格式:已存储 X AE / Y kAE)
double energyStored = data.getDouble(TAG_ENERGY_STORED);
double energyMax = data.getDouble(TAG_ENERGY_MAX);
double maxOutput = data.getDouble(TAG_ENERGY_OUTPUT);
tooltip.add(Component.translatable("thaumicenergistics.jade.energy_stored",
Platform.formatPower(energyStored, false),
Platform.formatPower(energyMax, false)));
// 可视化能量条 + 内嵌文字(FE 单位,1 AE = 2 FE);样式复刻 Jade 内置 EnergyStorageProvider.PROGRESS_BAR(深红渐变 + 灰色描边)
if (energyMax > 0) {
float pct = (float) Math.min(1.0, Math.max(0.0, energyStored / energyMax));
String feText = formatFE(energyStored * 2) + " / " + formatFE(energyMax * 2);
IElementHelper helper = IElementHelper.get();
tooltip.add(helper.progress(pct, Component.literal(feText),
helper.progressStyle().color(0xFFAA0000, 0xFF660000),
BoxStyle.getNestedBox(), true));
}
tooltip.add(Component.translatable("thaumicenergistics.jade.energy_output",
String.format("%.0f", maxOutput)));
boolean generating = data.getBoolean(TAG_GENERATING);
if (generating && energyStored < energyMax - 0.01) {
int burnTicks = data.getInt(TAG_BURN_TICKS);
int totalTicks = data.getInt(TAG_TOTAL_BURN);
double aePerTick = data.getDouble(TAG_AE_PER_TICK);
if (burnTicks > 0 && totalTicks > 0) {
double progress = 1.0 - (double) burnTicks / totalTicks;
int percent = (int) (progress * 100);
tooltip.add(Component.translatable("thaumicenergistics.jade.burning", percent, aePerTick));
}
} else if (energyStored >= energyMax - 0.01) {
tooltip.add(Component.translatable("thaumicenergistics.jade.tank_full"));
}
}
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileEssentiaVibrationChamber chamber) {
data.putInt(TAG_STORED, chamber.getStoredEssentia());
data.putInt(TAG_MAX, chamber.getMaxEssentia());
data.putInt(TAG_BURN_TICKS, chamber.getBurnTicksRemaining());
data.putInt(TAG_TOTAL_BURN, chamber.getTotalBurnTicks());
data.putDouble(TAG_AE_PER_TICK, chamber.getAePerTick());
data.putDouble(TAG_ENERGY_STORED, chamber.getStoredEnergy());
data.putDouble(TAG_ENERGY_MAX, chamber.getMaxEnergyStorage());
data.putDouble(TAG_ENERGY_OUTPUT, chamber.getMaxOutputPerTick());
data.putBoolean(TAG_GENERATING, chamber.isActive());
if (chamber.getSuctionType(null) != null) {
data.putString(TAG_ASPECT, chamber.getSuctionType(null).getTag());
}
}
}
@Override
public ResourceLocation getUid() {
return UID;
}
/**
* 将 FE 能量值格式化为无空格缩写:0 → "0FE"16000 → "16KFE"12345 → "12.3KFE"。
*/
private static String formatFE(double fe) {
final String[] prefixes = {"", "K", "M", "G", "T", "P"};
int idx = 0;
double v = fe;
while (v >= 1000 && idx < prefixes.length - 1) {
v /= 1000;
idx++;
}
String num = (v == Math.floor(v) && !Double.isInfinite(v))
? String.valueOf((long) v)
: String.format("%.1f", v);
return num + prefixes[idx] + "FE";
}
}
@@ -0,0 +1,149 @@
package thaumicenergistics.common.integration.tc;
import java.util.List;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.item.ItemStack;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
/**
* 奥术合成样板,存储在知识核心中。
* 从 1.7.10 ArcaneCraftingPattern 移植。
*/
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 int GRID_SIZE = 9;
/** 全局 RegistryAccess,用于 NBT 读写 */
private static final HolderLookup.Provider REGISTRY_ACCESS =
RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY);
protected AspectList aspects;
protected ItemStack[] ingredients = new ItemStack[GRID_SIZE];
protected ItemStack result = ItemStack.EMPTY;
protected boolean isValid;
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients) {
this.aspects = aspects.copy();
this.result = result.copy();
boolean hasValidInput = false;
for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) {
if (ingredients[i] != null && !ingredients[i].isEmpty()) {
this.ingredients[i] = ingredients[i].copy();
hasValidInput = true;
}
}
this.isValid = this.aspects.size() > 0 && !this.result.isEmpty() && hasValidInput;
}
public ArcaneCraftingPattern(CompoundTag data) {
this.aspects = new AspectList();
readFromNBT(data);
}
public boolean isPatternValid() {
return isValid;
}
public AspectList getAspects() {
return aspects;
}
public ItemStack getResult() {
return result;
}
public ItemStack[] getIngredients() {
return ingredients;
}
public Aspect[] getCachedAspects() {
return aspects.getAspects();
}
public int getAspectCost(Aspect aspect) {
return aspects.getAmount(aspect);
}
@SuppressWarnings("unchecked")
public static boolean canSubstituteFor(Object target, ItemStack input) {
if (target instanceof ItemStack targetStack) {
return ItemStack.isSameItemSameComponents(targetStack, input);
} else if (target instanceof List) {
List<ItemStack> items = (List<ItemStack>) target;
for (ItemStack item : items) {
if (ItemStack.isSameItemSameComponents(item, input)) return true;
}
}
return false;
}
public void readFromNBT(CompoundTag data) {
if (data == null) return;
this.ingredients = new ItemStack[GRID_SIZE];
this.isValid = true;
if (data.contains(NBTKEY_ASPECTS)) {
this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS));
}
if (this.aspects.size() == 0) {
this.isValid = false;
return;
}
for (int slot = 0; slot < GRID_SIZE; slot++) {
if (!data.contains(NBTKEY_INGREDIENT_NUM + slot)) continue;
CompoundTag ingTag = data.getCompound(NBTKEY_INGREDIENT_NUM + slot);
if (ingTag.isEmpty()) {
this.isValid = false;
return;
}
ItemStack stack = ItemStack.parseOptional(REGISTRY_ACCESS, ingTag);
if (stack.isEmpty()) {
this.isValid = false;
return;
}
this.ingredients[slot] = stack;
}
if (data.contains(NBTKEY_RESULT)) {
CompoundTag resultTag = data.getCompound(NBTKEY_RESULT);
this.result = ItemStack.parseOptional(REGISTRY_ACCESS, resultTag);
if (this.result.isEmpty()) {
this.isValid = false;
}
} else {
this.isValid = false;
}
}
public CompoundTag writeToNBT(CompoundTag data) {
CompoundTag aspectTag = new CompoundTag();
this.aspects.writeToNBT(aspectTag);
data.put(NBTKEY_ASPECTS, aspectTag);
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。
// 必须用返回值(之前用双参 save 并丢弃返回值,导致存了空 tag、配方反序列化全失败)。
CompoundTag ingTag = (CompoundTag) this.ingredients[i].save(REGISTRY_ACCESS);
data.put(NBTKEY_INGREDIENT_NUM + i, ingTag);
}
CompoundTag resultTag = (CompoundTag) this.result.save(REGISTRY_ACCESS);
data.put(NBTKEY_RESULT, resultTag);
return data;
}
}
@@ -0,0 +1,30 @@
package thaumicenergistics.common.integration.tc;
import net.minecraft.resources.ResourceLocation;
import thaumicenergistics.api.storage.IAspectStack;
import thaumicenergistics.api.storage.AspectStack;
import java.util.Optional;
/**
* 源质类型与数量转换辅助。
* 1.7.10 用 AE 气体/流体桥接;1.21.1 用直接类型映射。
*/
public final class EssentiaConversionHelper {
private EssentiaConversionHelper() {}
/** 每单位源质转移的 AE 能量成本。1.7.10: 0.3 AE/单位。 */
public static final double AE_PER_ESSENTIA = 0.3;
/** 标准 TC 源质罐容量(单位)。 */
public static final int JAR_CAPACITY = 64;
/** 标准 TC 药剂瓶容量(单位)。 */
public static final int PHIAL_CAPACITY = 8;
/**
* 在不同源质单位间转换。
* TODO: 映射到实际 TC Aspect API。
*/
public static Optional<IAspectStack> convert(IAspectStack input, String targetUnit) {
return Optional.of(input);
}
}
@@ -0,0 +1,156 @@
package thaumicenergistics.common.integration.tc;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.research.ResearchCategories;
import thaumcraft.api.research.ResearchItem;
import thaumcraft.api.research.ResearchPage;
import thaumcraft.common.research.TCResearchIndex;
import thaumicenergistics.ThaumicEnergistics;
/**
* 伪前置桩节点。在 ThE 的研究 tab 中创建一个占位节点,
* 其图标、名称、页面全部指向 TC 原版研究,使得 ThE 节点可以跨分类连线。
*
* 关键实现细节:
* - key 仍使用 PSEUDO_ 前缀(如 PSEUDO_VISPOWER),避免与原版研究在 TCResearchManager 的 CompoundTag 中冲突。
* - 图标(icon_item/icon_resource)直接使用原版研究的图标,在 registerApiResearch 时会被正确拷贝到 TCResearchIndex.Entry。
* - 页面(pages)直接使用原版研究的页面,使点击伪前置时能展示原版研究的内容。
* - 名称翻译通过 ThE 的 lang 文件中添加 tc.research_name.PSEUDO_* 条目指向原版翻译 key 来解决。
* - 解锁同步通过 ResearchCompletedEvent 事件监听实现,见 ThEThaumcraft.registerPseudoSync()。
*/
public class PseudoResearchItem extends ResearchItem {
/** 原版真实研究的 key,如 "DISTILESSENTIA" */
private final String realKey;
/** 原版真实研究所在分类,如 "ALCHEMY" */
private final String realCategory;
/** 指向的原版研究(用于 getName/getText/getPages 委托)。 */
private ResearchItem realResearch;
private PseudoResearchItem(String pseudoKey, String category, int column, int row,
ItemStack icon, String realKey, String realCategory) {
super(pseudoKey, category, new AspectList(), column, row, 1, icon);
this.realKey = realKey;
this.realCategory = realCategory;
// concealed: 当 parentsComplete 或 completed 时可见
this.setStub().setConcealed();
}
private PseudoResearchItem(String pseudoKey, String category, int column, int row,
ResourceLocation icon, String realKey, String realCategory) {
super(pseudoKey, category, new AspectList(), column, row, 1, icon);
this.realKey = realKey;
this.realCategory = realCategory;
this.setStub().setConcealed();
}
/**
* 创建一个指向 TC 原版研究的伪前置。
*
* @param pseudoKey 伪前置在 ThE tab 中的 key(如 "PSEUDO_DISTILESSENTIA"
* @param category ThE 研究分类(即 TAB)
* @param realKey TC 原版研究的 key(如 "DISTILESSENTIA"
* @param realCategory TC 原版研究所在分类(如 "ALCHEMY"
* @param column 伪前置的 X 坐标
* @param row 伪前置的 Y 坐标
*/
public static PseudoResearchItem newPseudo(String pseudoKey, String category,
String realKey, String realCategory,
int column, int row) {
var realCat = ResearchCategories.researchCategories.get(realCategory);
if (realCat == null) {
throw new IllegalArgumentException("TC category not found: " + realCategory);
}
ResearchItem realResearch = realCat.research.get(realKey);
if (realResearch == null) {
throw new IllegalArgumentException("TC research not found: " + realKey + " in " + realCategory);
}
PseudoResearchItem pseudo;
// 图标第一优先:真实研究对应的物品(research_index.json 的 item 字段,TC tab 中即用此图标)
ItemStack realIcon = resolveRealIconItem(realKey);
if (!realIcon.isEmpty()) {
pseudo = new PseudoResearchItem(pseudoKey, category, column, row, realIcon, realKey, realCategory);
} else if (realResearch.icon_item != null && !realResearch.icon_item.isEmpty()) {
pseudo = new PseudoResearchItem(pseudoKey, category, column, row, realResearch.icon_item, realKey, realCategory);
} else if (realResearch.icon_resource != null) {
pseudo = new PseudoResearchItem(pseudoKey, category, column, row, realResearch.icon_resource, realKey, realCategory);
} else if (realCat.icon != null) {
// 备选:真实研究所在分类的图标
pseudo = new PseudoResearchItem(pseudoKey, category, column, row, realCat.icon, realKey, realCategory);
} else {
// 最终备选:ThE 研究 tab 图标
pseudo = new PseudoResearchItem(pseudoKey, category, column, row,
ResourceLocation.fromNamespaceAndPath(thaumicenergistics.ThaumicEnergistics.MODID, "textures/research/tab_icon.png"),
realKey, realCategory);
}
// 保存真实研究引用:内容(名称/文字/页面)通过方法委托到真实研究(TC port 渲染调 getPages()
pseudo.realResearch = realResearch;
ThaumicEnergistics.LOG.debug("[Pseudo] {} ← {}:{} icon={}",
pseudoKey, realCategory, realKey,
pseudo.icon_item != null && !pseudo.icon_item.isEmpty()
? pseudo.icon_item.getDescriptionId() : String.valueOf(pseudo.icon_resource));
return pseudo;
}
/**
* 从 TC 的权威研究索引(research_index.json)解析真实研究对应的物品图标。
*/
private static ItemStack resolveRealIconItem(String realKey) {
try {
return TCResearchIndex.entry(realKey)
.map(entry -> {
String itemId = entry.item();
if (itemId == null || itemId.isEmpty()) return ItemStack.EMPTY;
ResourceLocation id = ResourceLocation.tryParse(itemId);
if (id == null) return ItemStack.EMPTY;
Item item = BuiltInRegistries.ITEM.get(id);
return (item == null || item == net.minecraft.world.item.Items.AIR) ? ItemStack.EMPTY : new ItemStack(item);
})
.orElse(ItemStack.EMPTY);
} catch (Exception e) {
return ItemStack.EMPTY;
}
}
public String realKey() { return realKey; }
public String realCategory() { return realCategory; }
// ===== 内容委托:TC port 渲染研究时调用 getName/getText/getPages,全部转发到真实研究 =====
@Override
public String getName() {
return realResearch != null ? realResearch.getName() : super.getName();
}
@Override
public String getText() {
return realResearch != null ? realResearch.getText() : super.getText();
}
@Override
public ResearchPage[] getPages() {
// 优先:原版研究 Java 页面(API 注册的研究有)
if (realResearch != null && realResearch.getPages() != null && realResearch.getPages().length > 0) {
return realResearch.getPages();
}
// 兜底:原版研究页面在 research_index.json 的 pages 字段(页面 key,如 tc.research_page.DISTILESSENTIA.1
// registerApiResearch 会从 ResearchPage.text 提取这些 key 作为 Entry.pages,渲染时据此显示原版内容
return TCResearchIndex.entry(realKey)
.map(entry -> entry.pages().stream()
.filter(p -> p != null && !p.isBlank())
.map(ResearchPage::new)
.toArray(ResearchPage[]::new))
.orElse(new ResearchPage[0]);
}
}
@@ -0,0 +1,165 @@
package thaumicenergistics.common.integration.tc;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IAspectContainer;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.AspectStack;
public final class TCReflection {
/** aspect tag → ResourceLocation 缓存(tag 有界:6 个源质 + 已注册方面)。 */
private static final Map<String, ResourceLocation> ASPECT_ID_CACHE = new ConcurrentHashMap<>();
private TCReflection() {}
@Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, int maxAmount) {
if (be == null) {
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: be is null");
return null;
}
if (!(be instanceof IAspectContainer container)) {
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: {} does not implement IAspectContainer", be.getClass().getName());
return null;
}
// ===== 路径A: 旧版 IAspectContainer API (getAspects) =====
AspectList aspectList = container.getAspects();
if (aspectList != null && aspectList.size() > 0) {
Aspect[] aspects = aspectList.getAspects();
if (aspects != null) {
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: found {} aspects via getAspects()", aspects.length);
for (Aspect aspect : aspects) {
if (aspect == null) continue;
int amt = aspectList.getAmount(aspect);
if (amt <= 0) continue;
int toExtract = Math.min(amt, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged();
ResourceLocation id = getAspectId(aspect);
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: SUCCESS (API) - extracted {} of {}", toExtract, id);
return id != null ? new AspectStack(id, toExtract) : null;
}
}
}
}
// ===== 路径B: TC 移植版反射回退 (getAspect/getAmount) =====
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: getAspects() empty, trying reflection fallback for {}", be.getClass().getName());
try {
java.lang.reflect.Method[] methods = getAspectMethods(be.getClass());
if (methods.length < 2) {
return null;
}
Aspect aspect = (Aspect) methods[0].invoke(be);
int amount = (int) methods[1].invoke(be);
if (aspect != null && amount > 0) {
int toExtract = Math.min(amount, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged();
ResourceLocation id = getAspectId(aspect);
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: SUCCESS (reflection) - extracted {} of {}", toExtract, id);
return id != null ? new AspectStack(id, toExtract) : null;
}
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: takeFromContainer returned false for {} of {}", toExtract, aspect.getTag());
} else {
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: reflection got aspect={}, amount={}", aspect, amount);
}
} catch (Exception e) {
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: reflection fallback error: {}", e.getMessage());
}
ThaumicEnergistics.LOG.debug("[TCReflection] extractEssentia: no extractable aspect found");
return null;
}
public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return;
if (!(be instanceof IAspectContainer container)) return;
Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return;
container.addToContainer(aspect, (int) stack.amount());
be.setChanged();
}
public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return 0;
if (!(be instanceof IAspectContainer container)) return 0;
Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return 0;
int toInsert = (int) stack.amount();
ThaumicEnergistics.LOG.debug("[TCReflection] insertEssentia: inserting {} of {} into {}", toInsert, stack.aspectId(), be.getClass().getName());
int leftover = container.addToContainer(aspect, toInsert);
int added = toInsert - leftover;
ThaumicEnergistics.LOG.debug("[TCReflection] insertEssentia: added={}, leftover={}", added, leftover);
if (added > 0) {
be.setChanged();
}
return added;
}
@Nullable
public static Aspect findAspectById(ResourceLocation id) {
if (id == null) return null;
if (Aspect.aspects == null) return null;
ensureAspectCache();
String lowerTag = id.getPath().toLowerCase(Locale.ROOT);
Aspect cached = ASPECT_BY_TAG.get(lowerTag);
if (cached != null) return cached;
// 缓存 miss:运行期(post-init)可能注册了新 aspect,回退一次实时遍历并补入缓存
for (Aspect aspect : Aspect.aspects.values()) {
if (aspect != null && aspect.getTag() != null && aspect.getTag().equalsIgnoreCase(id.getPath())) {
ASPECT_BY_TAG.putIfAbsent(lowerTag, aspect);
return aspect;
}
}
return null;
}
/** tag(小写) → Aspect 反向缓存,避免每次调用 O(n) 遍历 Aspect.aspects。 */
private static final Map<String, Aspect> ASPECT_BY_TAG = new ConcurrentHashMap<>();
private static volatile boolean aspectCacheBuilt = false;
private static void ensureAspectCache() {
if (aspectCacheBuilt) return;
synchronized (ASPECT_BY_TAG) {
if (aspectCacheBuilt) return;
for (Aspect aspect : Aspect.aspects.values()) {
if (aspect != null && aspect.getTag() != null) {
ASPECT_BY_TAG.putIfAbsent(aspect.getTag().toLowerCase(Locale.ROOT), aspect);
}
}
aspectCacheBuilt = true;
}
}
/** 按方块实体类缓存反射方法(getAspect/getAmount),避免每次调用 getMethod。 */
private static final Map<Class<?>, java.lang.reflect.Method[]> REFLECT_METHOD_CACHE = new ConcurrentHashMap<>();
private static java.lang.reflect.Method[] getAspectMethods(Class<?> clazz) {
return REFLECT_METHOD_CACHE.computeIfAbsent(clazz, c -> {
try {
return new java.lang.reflect.Method[] { c.getMethod("getAspect"), c.getMethod("getAmount") };
} catch (NoSuchMethodException e) {
return new java.lang.reflect.Method[0];
}
});
}
@Nullable
public static ResourceLocation getAspectId(@Nullable Aspect aspect) {
if (aspect == null) return null;
String tag = aspect.getTag();
if (tag == null || tag.isEmpty()) return null;
return ASPECT_ID_CACHE.computeIfAbsent(tag.toLowerCase(Locale.ROOT), t ->
ResourceLocation.fromNamespaceAndPath("thaumcraft", t));
}
}
@@ -0,0 +1,392 @@
package thaumicenergistics.common.integration.tc;
import java.util.HashMap;
import java.util.Map;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import thaumcraft.api.ThaumcraftApi;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.events.ResearchCompletedEvent;
import thaumcraft.api.research.ResearchCategories;
import thaumcraft.api.research.ResearchItem;
import thaumcraft.api.research.ResearchPage;
import thaumcraft.api.crafting.IArcaneRecipe;
import thaumcraft.api.crafting.InfusionRecipe;
import thaumcraft.api.crafting.CrucibleRecipe;
import thaumcraft.common.research.TCResearchManager;
import net.neoforged.neoforge.common.NeoForge;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.features.RecipeRegistration;
import thaumicenergistics.init.ModBlocks;
import thaumicenergistics.init.ModItems;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 神秘能源 魔导手册研究注册
* 严格按照 1.7.10 ResearchRegistry 布局移植
*
* 1.7.10 伪前置(TC原版)映射:
* DISTILESSENTIA = 要素蒸馏
* TUBEFILTER = 高级源质管道
* MIRROR = 魔镜
* INFUSION = 注魔
* WARDEDARCANA = 守护之奥术
* ALCHEMICALDUPLICATION = 炼金复制
* FOCUSFIRE = 法杖核心
* VISPOWER = 掌控魔力
* SCEPTRE = 工艺权杖
* COREUSE = 傀儡核心:使用
* COREGATHER = 傀儡核心:聚集
*/
public final class ThEThaumcraft {
private static final Logger LOG = LoggerFactory.getLogger("ThE-TC");
static final String TAB = "THAUMICENERGISTICS";
static final String MODID = ThaumicEnergistics.MODID;
/**
* 原版研究 key → 伪前置 key 的映射。
* 用于 ResearchCompletedEvent 事件监听:当原版研究解锁时,自动 complete 对应伪前置。
*/
private static final Map<String, String> REAL_TO_PSEUDO = new HashMap<>();
private ThEThaumcraft() {}
public static void init() {
LOG.info("Registering ThaumicEnergistics research...");
ResearchCategories.registerCategory(TAB,
ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/tab_icon.png"),
ResourceLocation.fromNamespaceAndPath("thaumcraft", "textures/gui/gui_researchback.png"));
registerScanEvent();
// ⚠ 必须先在当前 tab 注册伪前置节点(替代 TC 原版研究),
// 否则 setParents 跨分类引用时线画不出来。
registerPseudoParents();
// 注册事件监听:当 TC 原版研究解锁时,自动 complete 对应伪前置节点
registerPseudoSync();
// 严格按照 1.7.10 FeatureRegistry 构造函数中的注册顺序
// 1. FeatureThaumicEnergistics → BASEENERGISTICS (0,0)
registerBaseEnergistics();
// 2. FeatureAutocrafting_Arcane → ARCANE_ASSEMBLER + KNOWLEDGE_INSCRIBER
registerArcaneAssembler();
registerKnowledgeInscriber();
// 3. FeatureCells → ESSENTIASTORAGE (0,2) 向左延伸 1k/4k/16k/64k
registerEssentiaStorage();
// 4. FeatureACT → ARCANECRAFTINGTERMINAL (2,-1)
registerArcaneCraftingTerminal();
// 5. FeatureVisRelayInterface → VISINTERFACE (4,-1)
registerVisInterface();
// 6. FeatureEssentiaIOBuses → ESSENTIABUSES (-2,-2)
registerEssentiaBuses();
// 7. FeatureInfusionProvider → INFUSIONPROVIDER (-5,-2)
registerInfusionProvider();
// 8. FeatureEssentiaProvider → ESSENTIAPROVIDER (-2,-4)
registerEssentiaProvider();
// 9. FeatureEssentiaMonitoring → ESSENTIATERMINAL (-1,-4)
registerEssentiaTerminal();
// 10. FeatureConversionCores → DIGISENTIA (0,-2)
registerDigisentia();
// 12. FeatureWrenchFocus → FOCUS_AEWRENCH (-3,-7)
registerFocusAEWrench();
// 13. FeatureQuartzDupe → CERTUS_DUPE (-5,-5)
registerCertusDupe();
// 14. FeatureEssentiaVibrationChamber → VIBRATIONCHAMBER (1,-4)
registerEssentiaVibrationChamber();
// 15. FeatureAutocrafting_Essentia → DISTILLATIONENCODER (-4,-4)
registerDistillationEncoder();
// 16. FeatureGolemBackpack → GOLEMWIFIBACKPACK (0,-6)
registerGolemWifiBackpack();
LOG.info("ThaumicEnergistics research registered ({} nodes).", 13);
}
private static void registerScanEvent() {
ThaumcraftApi.registerScanEventhandler((scanner, level, player) ->
java.util.Optional.empty());
}
// ==================== 伪前置节点(跨分类引用 TC 原版研究) ====================
// 在 ThE 研究 tab 中创建占位节点,使跨分类连线正常。
// 坐标来自 1.7.10 ResearchRegistry.PseudoResearchTypes
//SCEPTRE(工艺权杖)、DISTILESSENTIA(要素蒸馏) 、WARDEDARCANA(守护之奥术)、VISPOWER(掌控魔力)、
// TUBEFILTER(高级源质管道)、INFUSION(注魔) 、MIRROR(魔镜)、COREUSE(傀儡核心:使用)
private static void registerPseudoParents() {
// ALCHEMY
registerPseudo("DISTILESSENTIA", "ALCHEMY", -2, 0);
registerPseudo("TUBEFILTER", "ALCHEMY", -3, 0);
registerPseudo("ALCHEMICALDUPLICATION", "ALCHEMY", -5, -6);
// ARTIFICE
registerPseudo("MIRROR", "ARTIFICE", -4, 0);
registerPseudo("INFUSION", "ARTIFICE", -6, 0);
registerPseudo("WARDEDARCANA", "ARTIFICE", 1, 2);
// THAUMATURGY
registerPseudo("FOCUSFIRE", "THAUMATURGY", -4, -7);
registerPseudo("VISPOWER", "THAUMATURGY", 4, 0);
registerPseudo("SCEPTRE", "THAUMATURGY", 5, 0);
// GOLEMANCY
registerPseudo("COREGATHER", "GOLEMANCY", 1, -6);
}
private static void registerPseudo(String realKey, String realCategory, int col, int row) {
String pseudoKey = "PSEUDO_" + realKey;
try {
PseudoResearchItem.newPseudo(pseudoKey, TAB, realKey, realCategory, col, row)
.registerResearchItem();
// 记录映射,供事件监听使用
REAL_TO_PSEUDO.put(realKey, pseudoKey);
} catch (Exception e) {
LOG.warn("伪前置注册失败: PSEUDO_{} (TC:{}/{})", realKey, realCategory, realKey);
}
}
/**
* 注册事件监听:当 TC 原版研究被 complete 时,自动 complete 对应的伪前置节点。
* 替代 1.7.10 的 sibling 机制——在 NeoForge 版本中 sibling 机制失效,
* 因为 {@code TCResearchIndex.Entry} 是不可变 record
* 修改 {@code ResearchItem.siblings} 不会更新到 {@code TCResearchIndex}。
*/
private static void registerPseudoSync() {
NeoForge.EVENT_BUS.addListener(ResearchCompletedEvent.class, (ResearchCompletedEvent rc) -> {
String completedKey = rc.researchKey();
// 跳过 clue 解锁(key 以 @ 开头)
if (completedKey.startsWith("@")) return;
// 跳过 PSEUDO_ 节点完成事件,防止 complete() 触发二次事件造成连锁
if (completedKey.startsWith("PSEUDO_")) return;
String pseudoKey = REAL_TO_PSEUDO.get(completedKey);
if (pseudoKey == null) return;
// 自动 complete 伪前置节点(不通知玩家)
try {
TCResearchManager.complete(rc.player(), pseudoKey, false);
} catch (Exception e) {
LOG.debug("伪前置自动解锁失败: {} → {}", completedKey, pseudoKey);
}
});
}
// ==================== 1. (0,0) BASEENERGISTICS 基础能源学====================
//无前置
private static void registerBaseEnergistics() {
new ResearchItem("BASEENERGISTICS", TAB,
new AspectList(),
0, 0,
0,
ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/tab_icon.png"))
.setRound().setAutoUnlock()
.setPages(new ResearchPage("tc.research_text.BASEENERGISTICS"))
.registerResearchItem();
}
// ==================== 2. (6,-1) ARCANE_ASSEMBLER 奥术装配器====================
// 前置: VISINTERFACE(要素中继接口) + SCEPTRE(工艺权杖)
private static void registerArcaneAssembler() {
new ResearchItem("ARCANEASSEMBLER", TAB,
new AspectList().add(Aspect.CRAFT, 10).add(Aspect.MAGIC, 10)
.add(Aspect.MECHANISM, 5).add(Aspect.ORDER, 5),
6, -1, 3, new ItemStack(ModItems.ARCANE_ASSEMBLER_ITEM.get()))
.setRound().setParents("VISINTERFACE", "PSEUDO_SCEPTRE")
.setPages(
new ResearchPage("tc.research_text.ARCANEASSEMBLER.stage.1"),
new ResearchPage(RecipeRegistration.ARCANE_ASSEMBLER),
new ResearchPage("tc.research_text.ARCANEASSEMBLER.stage.2"))
.registerResearchItem();
}
// ==================== 2. (6,0) KNOWLEDGE_INSCRIBER 知识记录仪====================
// 前置: ARCANEASSEMBLER(奥术装配器)
private static void registerKnowledgeInscriber() {
new ResearchItem("KNOWLEDGEINSCRIBER", TAB,
new AspectList().add(Aspect.MIND, 10).add(Aspect.MECHANISM, 5).add(Aspect.MAGIC, 5),
6, 0, 3, new ItemStack(ModItems.KNOWLEDGE_INSCRIBER_ITEM.get()))
.setRound().setParents("ARCANEASSEMBLER")
.setPages(
new ResearchPage("tc.research_text.KNOWLEDGEINSCRIBER"),
new ResearchPage(RecipeRegistration.KNOWLEDGE_CORE_ITEM),
new ResearchPage(RecipeRegistration.KNOWLEDGE_INSCRIBER_BLOCK))
.registerResearchItem();
}
// ==================== 3. (0,2) ESSENTIASTORAGE 源质存储====================
// 前置: BASEENERGISTICS(基础能源学) + DISTILESSENTIA(要素蒸馏) + WARDEDARCANA(守护之奥术)
private static void registerEssentiaStorage() {
new ResearchItem("ESSENTIASTORAGE", TAB,
new AspectList().add(Aspect.ORDER, 1).add(Aspect.MAGIC, 1),
0, 2, 2, new ItemStack(ModItems.ESSENTIA_CELL_1K.get()))
.setRound().setParents("BASEENERGISTICS", "PSEUDO_DISTILESSENTIA", "PSEUDO_WARDEDARCANA")
.setPages(
new ResearchPage("tc.research_text.ESSENTIASTORAGE"),
new ResearchPage(RecipeRegistration.STORAGE_COMPONENT_1K),
new ResearchPage(new IArcaneRecipe[]{RecipeRegistration.STORAGE_COMPONENT_4K, RecipeRegistration.STORAGE_COMPONENT_16K, RecipeRegistration.STORAGE_COMPONENT_64K}))
.registerResearchItem();
}
// ==================== 4. (2,-1) ARCANECRAFTINGTERMINAL 奥术合成终端====================
// 前置: BASEENERGISTICS(基础能源学)
private static void registerArcaneCraftingTerminal() {
new ResearchItem("ARCANECRAFTINGTERMINAL", TAB,
new AspectList().add(Aspect.CRAFT, 10).add(Aspect.MAGIC, 10).add(Aspect.MECHANISM, 5),
2, -1, 2, new ItemStack(ModItems.ARCANE_CRAFTING_TERMINAL_ITEM.get()))
.setRound().setParents("BASEENERGISTICS")
.setPages(
new ResearchPage("tc.research_text.ARCANECRAFTINGTERMINAL.stage.1"),
new ResearchPage(RecipeRegistration.ARCANE_CRAFTING_TERMINAL),
new ResearchPage("tc.research_text.ARCANECRAFTINGTERMINAL.stage.2"))
.registerResearchItem();
}
// ==================== 5. (4,-1) VISINTERFACE 要素中继接口====================
// 前置: ARCANECRAFTINGTERMINAL(奥术合成终端) + VISPOWER(掌控魔力)
private static void registerVisInterface() {
new ResearchItem("VISINTERFACE", TAB,
new AspectList().add(Aspect.AURA, 10).add(Aspect.MAGIC, 5).add(Aspect.MECHANISM, 5),
4, -1, 3, new ItemStack(ModItems.VIS_INTERFACE.get()))
.setRound().setParents("ARCANECRAFTINGTERMINAL", "PSEUDO_VISPOWER")
.setPages(
new ResearchPage("tc.research_text.VISINTERFACE"),
new ResearchPage(RecipeRegistration.VIS_INTERFACE))
.registerResearchItem();
}
// ==================== 6. (-2,-2) ESSENTIABUSES 源质总线====================
// 前置: DIGISENTIA(数字化源质) + TUBEFILTER(高级源质管道)
private static void registerEssentiaBuses() {
new ResearchItem("ESSENTIABUSES", TAB,
new AspectList().add(Aspect.MECHANISM, 5).add(Aspect.EXCHANGE, 5).add(Aspect.MAGIC, 3),
-2, -2, 2, new ItemStack(ModItems.ESSENTIA_IMPORT_BUS.get()))
.setRound().setParents("DIGISENTIA", "PSEUDO_TUBEFILTER")
.setPages(
new ResearchPage("tc.research_text.ESSENTIABUSES.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_IMPORT_BUS),
new ResearchPage(RecipeRegistration.ESSENTIA_EXPORT_BUS),
new ResearchPage(RecipeRegistration.ESSENTIA_STORAGE_BUS),
new ResearchPage("tc.research_text.ESSENTIABUSES.stage.2"))
.registerResearchItem();
}
// ==================== 7. (-5,-2) INFUSIONPROVIDER 注魔供应器====================
// 前置: ESSENTIABUSES(源质总线) + INFUSION(注魔) + MIRROR(魔镜)
private static void registerInfusionProvider() {
new ResearchItem("INFUSIONPROVIDER", TAB,
new AspectList().add(Aspect.MECHANISM, 10).add(Aspect.MAGIC, 10)
.add(Aspect.ORDER, 8).add(Aspect.EXCHANGE, 5),
-5, -2, 3, new ItemStack(ModItems.INFUSION_PROVIDER_ITEM.get()))
.setRound().setParents("ESSENTIABUSES", "PSEUDO_INFUSION", "PSEUDO_MIRROR")
.setPages(
new ResearchPage("tc.research_text.INFUSIONPROVIDER.stage.1"),
new ResearchPage(RecipeRegistration.INFUSION_PROVIDER),
new ResearchPage("tc.research_text.INFUSIONPROVIDER.stage.2"))
.registerResearchItem();
}
// ==================== 8. (-2,-4) ESSENTIAPROVIDER 源质供应器====================
// 前置: ESSENTIABUSES(源质总线)
private static void registerEssentiaProvider() {
new ResearchItem("ESSENTIAPROVIDER", TAB,
new AspectList().add(Aspect.MECHANISM, 8).add(Aspect.MAGIC, 8).add(Aspect.EXCHANGE, 5),
-2, -4, 3, new ItemStack(ModItems.ESSENTIA_PROVIDER_ITEM.get()))
.setRound().setParents("ESSENTIABUSES")
.setPages(
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER),
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.2"))
.registerResearchItem();
}
// ==================== 9. (-1,-4) ESSENTIATERMINAL 源质终端====================
// 前置: DIGISENTIA(数字化源质)
private static void registerEssentiaTerminal() {
new ResearchItem("ESSENTIATERMINAL", TAB,
new AspectList().add(Aspect.MAGIC, 5).add(Aspect.ORDER, 3).add(Aspect.MIND, 3),
-1, -4, 2, new ItemStack(ModItems.ESSENTIA_TERMINAL_ITEM.get()))
.setRound().setParents("DIGISENTIA")
.setPages(
new ResearchPage("tc.research_text.ESSENTIATERMINAL.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_TERMINAL),
new ResearchPage(RecipeRegistration.ESSENTIA_LEVEL_EMITTER),
new ResearchPage("tc.research_text.ESSENTIATERMINAL.stage.2"))
.registerResearchItem();
}
// ==================== 10. (0,-2) DIGISENTIA 数字化源质====================
// 前置: BASEENERGISTICS(基础能源学) + DISTILESSENTIA(要素蒸馏)
private static void registerDigisentia() {
new ResearchItem("DIGISENTIA", TAB,
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.MAGIC, 3),
0, -2, 1, new ItemStack(ModItems.DIFFUSION_CORE.get()))
.setRound().setParents("BASEENERGISTICS", "PSEUDO_DISTILESSENTIA")
.setPages(
new ResearchPage("tc.research_text.DIGISENTIA.stage.1"),
new ResearchPage(RecipeRegistration.DIFFUSION_CORE),
new ResearchPage(RecipeRegistration.COALESCENCE_CORE),
new ResearchPage("tc.research_text.DIGISENTIA.stage.2"))
.registerResearchItem();
}
// ==================== 12. (-3,-7) FOCUS_AEWRENCH 法杖核心 AE扳手====================
// 前置: BASEENERGISTICS(基础能源学) + FOCUSFIRE(法杖核心)
private static void registerFocusAEWrench() {
new ResearchItem("FOCUS_AEWRENCH", TAB,
new AspectList().add(Aspect.TOOL, 5).add(Aspect.ENERGY, 5).add(Aspect.MECHANISM, 3),
-3, -7, 3, new ItemStack(ModItems.FOCUS_AEWRENCH.get()))
.setRound().setParents("PSEUDO_FOCUSFIRE").setParentsHidden("BASEENERGISTICS")
.setPages(
new ResearchPage("tc.research_text.FOCUS_AEWRENCH"),
new ResearchPage(RecipeRegistration.FOCUS_AEWRENCH_RECIPE))
.registerResearchItem();
}
// ==================== 13. (-5,-5) CERTUS_DUPE 赛特斯石英复制====================
// 前置: ALCHEMICALDUPLICATION(炼金复制) + BASEENERGISTICS(基础能源学)
private static void registerCertusDupe() {
new ResearchItem("CERTUS_DUPE", TAB,
new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.MAGIC, 5).add(Aspect.EXCHANGE, 5),
-5, -5, 2, new ItemStack(appeng.core.definitions.AEItems.CERTUS_QUARTZ_CRYSTAL.asItem()))
.setRound().setParents("PSEUDO_ALCHEMICALDUPLICATION").setParentsHidden("BASEENERGISTICS")
.setPages(new ResearchPage("tc.research_text.CERTUS_DUPE"))
.registerResearchItem();
}
// ==================== 14. (1,-4) VIBRATIONCHAMBER 源质谐振仓====================
// 前置: DIGISENTIA
private static void registerEssentiaVibrationChamber() {
new ResearchItem("ESSENTIAVIBRATIONCHAMBER", TAB,
new AspectList().add(Aspect.ENERGY, 8).add(Aspect.MECHANISM, 5).add(Aspect.FIRE, 3),
1, -4, 3, new ItemStack(ModItems.ESSENTIA_VIBRATION_CHAMBER_ITEM.get()))
.setRound().setParents("DIGISENTIA")
.setPages(
new ResearchPage("tc.research_text.ESSENTIAVIBRATIONCHAMBER"),
new ResearchPage(RecipeRegistration.ESSENTIA_VIBRATION_CHAMBER))
.registerResearchItem();
}
// ==================== 15. (-4,-4) DISTILLATIONENCODER 蒸馏编码台====================
// 前置: ESSENTIABUSES
private static void registerDistillationEncoder() {
new ResearchItem("DISTILLATIONENCODER", TAB,
new AspectList().add(Aspect.MECHANISM, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
-4, -4, 3, new ItemStack(ModItems.DISTILLATION_ENCODER_ITEM.get()))
.setRound().setParents("ESSENTIABUSES")
.setPages(
new ResearchPage("tc.research_text.DISTILLATIONENCODER"),
new ResearchPage(RecipeRegistration.DISTILLATION_ENCODER_BLOCK))
.registerResearchItem();
}
// ==================== 16. (0,-6) GOLEMWIFIBACKPACK 无线傀儡背包====================
// 前置: DIGISENTIA + COREGATHER(傀儡核心:聚集)
private static void registerGolemWifiBackpack() {
new ResearchItem("GOLEMWIFIBACKPACK", TAB,
new AspectList().add(Aspect.MAGIC, 5).add(Aspect.MECHANISM, 5).add(Aspect.MIND, 3),
0, -6, 3, new ItemStack(ModItems.GOLEM_WIFI_BACKPACK.get()))
.setRound().setParents("PSEUDO_COREGATHER").setParentsHidden("DIGISENTIA")
.setPages(
new ResearchPage("tc.research_text.GOLEMWIFI"),
new ResearchPage(RecipeRegistration.GOLEM_WIFI_BACKPACK_RECIPE))
.registerResearchItem();
}
}
@@ -0,0 +1,85 @@
package thaumicenergistics.common.integration.tc;
import net.minecraft.core.BlockPos;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.visnet.TileVisNode;
import thaumcraft.api.visnet.VisNetHandler;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.common.parts.VisInterfacePart;
import java.lang.ref.WeakReference;
/**
* 注册到 {@link VisNetHandler} 的虚拟 TC vis 源,位于 Vis Interface "provider" 端前方。
* 附近 TC 机器(法杖、奥术工作台等)吸取 vis 时,请求转交给所属 {@link VisInterfacePart}
* 由其从所面对(或 P2P 连接的另一端)的 TC 中继抽取 vis。
*
* 自 1.7.10 (RV3) 移植,适配 1.21.1 TileVisNode API。代理不放置为真实方块实体;
* 用宿主方块坐标构造,手动注入 level,使 {@link VisNetHandler#addSource} 能按 WorldCoordinates 存储。
*/
public class VisProviderProxy extends TileVisNode {
/**
* vis 可被抽取的距离(格)。与 TC 中继范围一致,保证附近机器可用。
*/
private static final int VIS_RANGE = 8;
/**
* 所属 vis interface 部件。用弱引用持有,避免代理使部件(及所在 chunk)无法回收。
*/
private WeakReference<VisInterfacePart> owner;
public VisProviderProxy(BlockPos pos, BlockState state) {
super(ModBlockEntities.VIS_PROVIDER_PROXY.get(), pos, state);
this.owner = new WeakReference<>(null);
}
/**
* 构造后绑定所属部件与世界(BlockEntityType supplier 无法传入 owner,故在此注入)。
*/
public void bind(VisInterfacePart part, Level level) {
this.owner = new WeakReference<>(part);
// 真实方块实体的 level 由 chunk loader 设置;虚拟代理需手动注入。
this.setLevel(level);
}
@Override
public int getRange() {
return VisProviderProxy.VIS_RANGE;
}
@Override
public boolean isSource() {
return true;
}
/**
* 机器请求 vis 时由 {@link VisNetHandler#drainVis} 调用。
* 转交所属部件执行实际的中继抽取(消耗 AE 能量)并处理递归保护。
*/
@Override
public int consumeVis(Aspect aspect, int centivis) {
VisInterfacePart part = this.owner.get();
if (part == null || !part.isActive()) {
return 0;
}
ResourceLocation aspectId = ResourceLocation.fromNamespaceAndPath("thaumcraft", aspect.tag());
return part.consumeVis(aspectId, centivis);
}
/**
* 无焦点过滤:供应任意要素的 vis(对应常见无焦点中继)。
* 返回 -1 保持代理与所有请求节点兼容。
*/
@Override
public byte getAttunement() {
return -1;
}
@Override
public void invalidate() {
VisNetHandler.removeNode(this);
}
}
@@ -0,0 +1,148 @@
package thaumicenergistics.common.inventory;
import java.util.ArrayList;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.Tag;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.init.ModItems;
/**
* 管理知识核心中存储的奥术合成样板。
* 从 1.7.10 HandlerKnowledgeCore 移植。
*/
public class HandlerKnowledgeCore {
private static final String NBTKEY_PATTERNS = "Patterns";
public static final int MAXIMUM_STORED_PATTERNS = 21;
private final ArrayList<ArcaneCraftingPattern> patterns = new ArrayList<>(MAXIMUM_STORED_PATTERNS);
private ItemStack kCore;
public HandlerKnowledgeCore() {}
public HandlerKnowledgeCore(ItemStack kCore) {
open(kCore);
}
public void open(ItemStack kCore) {
close();
this.kCore = kCore;
loadKCoreData();
}
public void close() {
this.kCore = null;
this.patterns.clear();
}
public boolean hasCore() {
return this.kCore != null;
}
public boolean isHandlingCore(ItemStack kCore) {
if (this.kCore == null) return false;
if (kCore == null || kCore.isEmpty()) return false;
if (!kCore.is(ModItems.KNOWLEDGE_CORE.get())) return false;
return ItemStack.matches(this.kCore, kCore);
}
public boolean hasRoomToStorePattern() {
return this.patterns.size() < MAXIMUM_STORED_PATTERNS;
}
public boolean hasPatternFor(ItemStack resultStack) {
return getPatternForItem(resultStack) != null;
}
public ArcaneCraftingPattern getPatternForItem(ItemStack resultStack) {
for (ArcaneCraftingPattern p : this.patterns) {
if (p != null && p.getResult() != null && !p.getResult().isEmpty()) {
if (ItemStack.isSameItem(p.getResult(), resultStack)) {
return p;
}
}
}
return null;
}
public ArrayList<ArcaneCraftingPattern> getPatterns() {
return this.patterns;
}
public ArrayList<ItemStack> getStoredOutputs() {
ArrayList<ItemStack> results = new ArrayList<>();
for (ArcaneCraftingPattern p : this.patterns) {
if (p != null && p.getResult() != null && !p.getResult().isEmpty()) {
results.add(p.getResult().copy());
}
}
return results;
}
public void addPattern(ArcaneCraftingPattern pattern) {
if (pattern == null || !pattern.isPatternValid()) return;
if (!hasRoomToStorePattern()) return;
ArcaneCraftingPattern existing = getPatternForItem(pattern.getResult());
if (existing == null) {
this.patterns.add(pattern);
saveKCoreData();
}
}
public void removePattern(ArcaneCraftingPattern pattern) {
if (this.patterns.remove(pattern)) {
saveKCoreData();
}
}
private CompoundTag getOrCreateNBT() {
CustomData customData = this.kCore.get(DataComponents.CUSTOM_DATA);
if (customData == null) {
return new CompoundTag();
}
return customData.copyTag();
}
private void setNBT(CompoundTag tag) {
this.kCore.set(DataComponents.CUSTOM_DATA, CustomData.of(tag));
}
private void loadKCoreData() {
this.patterns.clear();
CompoundTag data = getOrCreateNBT();
if (data.contains(NBTKEY_PATTERNS)) {
ListTag plist = data.getList(NBTKEY_PATTERNS, Tag.TAG_COMPOUND);
for (int i = 0; i < plist.size(); i++) {
try {
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(plist.getCompound(i));
if (pattern.isPatternValid()) {
this.patterns.add(pattern);
}
} catch (Exception ignored) {}
}
}
}
private void saveKCoreData() {
CompoundTag data = getOrCreateNBT();
ListTag plist = new ListTag();
for (ArcaneCraftingPattern pattern : this.patterns) {
if (pattern == null || !pattern.isPatternValid()) continue;
plist.add(pattern.writeToNBT(new CompoundTag()));
}
if (!plist.isEmpty()) {
data.put(NBTKEY_PATTERNS, plist);
} else {
data.remove(NBTKEY_PATTERNS);
}
setNBT(data);
}
}
@@ -0,0 +1,226 @@
package thaumicenergistics.common.items;
import java.util.ArrayList;
import java.util.Optional;
import java.util.Set;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.inventory.tooltip.TooltipComponent;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import appeng.api.config.Actionable;
import appeng.api.config.FuzzyMode;
import appeng.api.stacks.AEKeyType;
import appeng.api.stacks.GenericStack;
import appeng.api.stacks.KeyCounter;
import appeng.api.storage.cells.IBasicCellItem;
import appeng.api.upgrades.IUpgradeInventory;
import appeng.api.upgrades.UpgradeInventories;
import appeng.items.contents.CellConfig;
import appeng.me.cells.BasicCellInventory;
import appeng.util.ConfigInventory;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.config.ThEConfig;
import org.jetbrains.annotations.Nullable;
import java.util.List;
public class ItemEssentiaCell extends Item implements IBasicCellItem {
private final String tier;
private final int totalBytes;
private final int bytesPerType;
private final int totalTypes;
private final double idleDrain;
public ItemEssentiaCell(Item.Properties props, String tier, int kilobytes, int bytesPerType, int totalTypes, double idleDrain) {
super(props.stacksTo(1));
this.tier = tier;
this.totalBytes = kilobytes * 1024;
this.bytesPerType = bytesPerType;
this.totalTypes = totalTypes;
this.idleDrain = idleDrain;
}
@Override
public AEKeyType getKeyType() {
return AEssentiaKeyType.INSTANCE;
}
@Override
public int getBytes(ItemStack cellItem) {
return this.totalBytes;
}
@Override
public int getBytesPerType(ItemStack cellItem) {
return this.bytesPerType;
}
@Override
public int getTotalTypes(ItemStack cellItem) {
return this.totalTypes;
}
@Override
public double getIdleDrain() {
return this.idleDrain;
}
@Override
public boolean isStorageCell(ItemStack i) {
return true;
}
@Override
public boolean isBlackListed(ItemStack cellItem, appeng.api.stacks.AEKey requestedAddition) {
if (requestedAddition instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey) {
return false;
}
return true;
}
@Override
public IUpgradeInventory getUpgrades(ItemStack is) {
return UpgradeInventories.forItem(is, 3);
}
@Override
public ConfigInventory getConfigInventory(ItemStack is) {
return CellConfig.create(Set.of(AEssentiaKeyType.INSTANCE), is);
}
@Override
public FuzzyMode getFuzzyMode(ItemStack is) {
return FuzzyMode.IGNORE_ALL;
}
@Override
public void setFuzzyMode(ItemStack is, FuzzyMode fzMode) {
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext ctx, List<Component> tooltip, TooltipFlag flag) {
this.addCellInformationToTooltip(stack, tooltip);
}
@Override
public Optional<TooltipComponent> getTooltipImage(ItemStack stack) {
// 渲染顶部源质图标(前5种存量最高的源质)
return this.getCellTooltipImage(stack);
}
public String getTier() { return tier; }
// ===== 分区辅助(基于 AE2 CellConfig,与 ME 终端分区互通) =====
@Nullable
private static ItemEssentiaCell getCellItem(ItemStack stack) {
return stack.getItem() instanceof ItemEssentiaCell cell ? cell : null;
}
/** 获取 cell 的分区库存(63 槽 ConfigInventory),非 cell 返回 null。 */
@Nullable
public static ConfigInventory getPartitionInventory(ItemStack stack) {
ItemEssentiaCell cell = getCellItem(stack);
return cell == null ? null : cell.getConfigInventory(stack);
}
/** 获取分区列表(aspect 的 ResourceLocation)。 */
public static List<ResourceLocation> getPartitionAspects(ItemStack stack) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv == null) return List.of();
List<ResourceLocation> result = new ArrayList<>();
for (int i = 0; i < inv.size(); i++) {
if (inv.getKey(i) instanceof AEssentiaKey ek) result.add(ek.getId());
}
return result;
}
/** 添加一个 aspect 到分区;已存在或满返回 false。 */
public static boolean addAspectToPartition(ItemStack stack, ResourceLocation aspectId) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv == null) return false;
AEssentiaKey key = AEssentiaKey.of(aspectId);
for (int i = 0; i < inv.size(); i++) {
if (key.equals(inv.getKey(i))) return false;
}
for (int i = 0; i < inv.size(); i++) {
if (inv.getKey(i) == null) {
inv.insert(i, key, 1, Actionable.MODULATE);
return true;
}
}
return false;
}
/** 从分区移除一个 aspect;不存在返回 false。 */
public static boolean removeAspectFromPartition(ItemStack stack, ResourceLocation aspectId) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv == null) return false;
for (int i = 0; i < inv.size(); i++) {
if (inv.getKey(i) instanceof AEssentiaKey ek && ek.getId().equals(aspectId)) {
inv.setStack(i, null);
return true;
}
}
return false;
}
/** 用 to 替换分区中的 fromfrom 不存在返回 false。 */
public static boolean replaceAspectInPartition(ItemStack stack, ResourceLocation from, ResourceLocation to) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv == null) return false;
AEssentiaKey newKey = AEssentiaKey.of(to);
for (int i = 0; i < inv.size(); i++) {
if (inv.getKey(i) instanceof AEssentiaKey ek && ek.getId().equals(from)) {
inv.setStack(i, new GenericStack(newKey, 1));
return true;
}
}
return false;
}
/** 清空全部分区。 */
public static void clearPartitioning(ItemStack stack) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv != null) inv.clear();
}
/** 将 cell 当前存储的源质设为分区(对应 1.7.10 的 partitionToCellContents)。 */
public static void partitionToContents(ItemStack stack) {
ConfigInventory inv = getPartitionInventory(stack);
if (inv == null) return;
inv.clear();
BasicCellInventory cellInv = BasicCellInventory.createInventory(stack, null);
if (cellInv == null) return;
KeyCounter counter = new KeyCounter();
cellInv.getAvailableStacks(counter);
int slot = 0;
for (var entry : counter) {
if (entry.getKey() instanceof AEssentiaKey ek && slot < inv.size()) {
inv.insert(slot++, ek, 1, Actionable.MODULATE);
}
}
}
// ===== 工厂方法 =====
public static ItemEssentiaCell create1k(Item.Properties props) {
return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5);
}
public static ItemEssentiaCell create4k(Item.Properties props) {
return new ItemEssentiaCell(props, "4k", 4, 8, ThEConfig.CELL_MAX_TYPES, 1.0);
}
public static ItemEssentiaCell create16k(Item.Properties props) {
return new ItemEssentiaCell(props, "16k", 16, 8, ThEConfig.CELL_MAX_TYPES, 1.5);
}
public static ItemEssentiaCell create64k(Item.Properties props) {
return new ItemEssentiaCell(props, "64k", 64, 8, ThEConfig.CELL_MAX_TYPES, 2.0);
}
public static ItemEssentiaCell createCreative(Item.Properties props) {
return new ItemEssentiaCell(props, "creative", Integer.MAX_VALUE / 1024, 8, 63, 0.0);
}
}
@@ -0,0 +1,184 @@
package thaumicenergistics.common.items;
import appeng.blockentity.AEBaseBlockEntity;
import net.minecraft.ChatFormatting;
import net.minecraft.core.BlockPos;
import net.minecraft.core.particles.ParticleTypes;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.ClipContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.vis.TCVisHelper;
import java.util.List;
public class ItemFocusAEWrench extends TCFunctionalItems.FocusItem {
private static final AspectList CAST_COST = new AspectList().add(Aspect.FIRE, 10).add(Aspect.AIR, 10);
public ItemFocusAEWrench() {
super(TCFunctionalItems.FocusEffect.FIRE, new Properties().stacksTo(1));
}
@Override
public int focusColor(ItemStack stack, float animationTicks) {
return Aspect.ENERGY.getColor();
}
@Override
public TCFunctionalItems.FocusCastingAnimation castingAnimation(ItemStack stack) {
return TCFunctionalItems.FocusCastingAnimation.WAVE;
}
@Override
public boolean isVisCostPerTick(ItemStack stack) {
return false;
}
@Override
public AspectList visCost(ItemStack stack) {
return CAST_COST;
}
@Override
public boolean requiresContinuousUse(ItemStack stack) {
return false;
}
@Override
public void clearContinuousState(ItemStack wandStack) {
}
@Override
public boolean castContinuousFromWand(Level level, Player player, ItemStack wandStack,
TCFunctionalItems.WandCastingItem wand, int remainingUseDuration) {
return false;
}
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
InteractionHand otherHand = hand == InteractionHand.MAIN_HAND ? InteractionHand.OFF_HAND : InteractionHand.MAIN_HAND;
ItemStack other = player.getItemInHand(otherHand);
Item item = other.getItem();
if (item instanceof TCFunctionalItems.WandCastingItem wand) {
if (!level.isClientSide && wand.installFocus(other, stack, player)) {
level.playSound(null, player.blockPosition(),
net.minecraft.sounds.SoundEvents.ITEM_FRAME_ADD_ITEM,
net.minecraft.sounds.SoundSource.PLAYERS, 0.45f, 1.2f);
}
return InteractionResultHolder.sidedSuccess(stack, level.isClientSide);
}
return InteractionResultHolder.pass(stack);
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, List<Component> tooltip, TooltipFlag flag) {
AspectList cost = this.visCost(stack);
MutableComponent costLine = Component.translatable(
this.isVisCostPerTick(stack) ? "item.Focus.cost2" : "item.Focus.cost1")
.append(":").withStyle(ChatFormatting.GRAY);
for (Aspect aspect : cost.aspects()) {
costLine = costLine.append(Component.literal(
" " + TCVisHelper.formatCentivis(cost.amount(aspect)))
.withStyle(style -> style.withColor(aspect.color())));
}
tooltip.add(costLine);
super.appendHoverText(stack, context, tooltip, flag);
}
@Override
public boolean castFromWand(Level level, Player player, ItemStack wandStack,
TCFunctionalItems.WandCastingItem wand) {
ItemStack focusStack = wand.getFocus(wandStack);
if (focusStack.getItem() != this) {
return false;
}
BlockHitResult hit = rayTrace(player, level, 24.0);
if (hit.getType() != HitResult.Type.BLOCK) {
return false;
}
if (!player.isShiftKeyDown()) {
return false;
}
AspectList cost = this.visCost(focusStack);
if (!wand.consumeVisCost(wandStack, player, cost, false)) {
player.displayClientMessage(
Component.translatable("message.thaumcraft.wand.not_enough_vis"), true);
return false;
}
if (!wand.consumeVisCost(wandStack, player, cost, true)) {
return false;
}
BlockPos pos = hit.getBlockPos();
boolean handled = handleWrenchAction(level, player, hit, wandStack);
if (handled) {
spawnBeamParticles(level, player, pos);
return true;
}
return false;
}
private boolean handleWrenchAction(Level level, Player player, BlockHitResult hit, ItemStack wandStack) {
BlockPos pos = hit.getBlockPos();
var blockEntity = level.getBlockEntity(pos);
if (blockEntity instanceof AEBaseBlockEntity aeBlockEntity) {
InteractionResult result = aeBlockEntity.disassembleWithWrench(player, level, hit, wandStack);
return result.consumesAction();
}
return false;
}
public static void spawnBeamParticles(Level level, Player player, BlockPos targetPos) {
if (!(level instanceof ServerLevel serverLevel) || !(player instanceof ServerPlayer serverPlayer)) {
return;
}
Vec3 start = player.getEyePosition(1.0f);
Vec3 end = Vec3.atCenterOf(targetPos);
Vec3 direction = end.subtract(start);
double distance = direction.length();
if (distance < 0.5) {
return;
}
Vec3 step = direction.normalize().scale(0.5);
Vec3 current = start.add(step);
int steps = (int) (distance / 0.5);
for (int i = 0; i < steps; i++) {
serverLevel.sendParticles(serverPlayer, ParticleTypes.END_ROD, true,
current.x, current.y, current.z,
1, 0.0, 0.0, 0.0, 0.0);
current = current.add(step);
}
}
private static BlockHitResult rayTrace(Player player, Level level, double range) {
Vec3 eye = player.getEyePosition(1.0f);
Vec3 end = eye.add(player.getLookAngle().scale(range));
return level.clip(new ClipContext(eye, end, ClipContext.Block.OUTLINE, ClipContext.Fluid.NONE, player));
}
}
@@ -0,0 +1,57 @@
package thaumicenergistics.common.items;
import net.minecraft.core.GlobalPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import appeng.api.features.IGridLinkableHandler;
import appeng.api.ids.AEComponents;
import appeng.core.localization.GuiText;
import appeng.core.localization.Tooltips;
import java.util.List;
public class ItemGolemWirelessBackpack extends Item {
public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler();
public ItemGolemWirelessBackpack() {
super(new Item.Properties().stacksTo(1).durability(0));
}
@Override
public void appendHoverText(ItemStack stack, TooltipContext context, List<Component> lines, TooltipFlag flag) {
super.appendHoverText(stack, context, lines, flag);
if (getLinkedPosition(stack) == null) {
lines.add(Tooltips.of(GuiText.Unlinked, Tooltips.RED));
} else {
lines.add(Tooltips.of(GuiText.Linked, Tooltips.GREEN));
}
}
public GlobalPos getLinkedPosition(ItemStack stack) {
return stack.get(AEComponents.WIRELESS_LINK_TARGET);
}
public static boolean isLinked(ItemStack stack) {
return stack.get(AEComponents.WIRELESS_LINK_TARGET) != null;
}
private static class LinkableHandler implements IGridLinkableHandler {
@Override
public boolean canLink(ItemStack stack) {
return stack.getItem() instanceof ItemGolemWirelessBackpack;
}
@Override
public void link(ItemStack stack, GlobalPos pos) {
stack.set(AEComponents.WIRELESS_LINK_TARGET, pos);
}
@Override
public void unlink(ItemStack stack) {
stack.remove(AEComponents.WIRELESS_LINK_TARGET);
}
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
import java.util.List;
public class PartItemArcaneCraftingTerminal extends Item implements IPartItem<ArcaneCraftingTerminalPart> {
public PartItemArcaneCraftingTerminal(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<ArcaneCraftingTerminalPart> getPartClass() {
return ArcaneCraftingTerminalPart.class;
}
@Override
public ArcaneCraftingTerminalPart createPart() {
return new ArcaneCraftingTerminalPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable Item.TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.arcane_crafting_terminal.desc"));
}
}
@@ -0,0 +1,49 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.EssentiaExportBusPart;
import java.util.List;
public class PartItemEssentiaExportBus extends Item implements IPartItem<EssentiaExportBusPart> {
public static PartItemEssentiaExportBus create() {
return new PartItemEssentiaExportBus(new Item.Properties().stacksTo(64));
}
public PartItemEssentiaExportBus(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<EssentiaExportBusPart> getPartClass() {
return EssentiaExportBusPart.class;
}
// 传 this:本类实现了 IPartItem<EssentiaExportBusPart>
@Override
public EssentiaExportBusPart createPart() {
return new EssentiaExportBusPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable Item.TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_export_bus.desc"));
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_export_bus.hint"));
}
}
@@ -0,0 +1,49 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.EssentiaImportBusPart;
import java.util.List;
public class PartItemEssentiaImportBus extends Item implements IPartItem<EssentiaImportBusPart> {
public static PartItemEssentiaImportBus create() {
return new PartItemEssentiaImportBus(new Item.Properties().stacksTo(64));
}
public PartItemEssentiaImportBus(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<EssentiaImportBusPart> getPartClass() {
return EssentiaImportBusPart.class;
}
// 构造器要求 IPartItem<?> partItem,传 this 即可
@Override
public EssentiaImportBusPart createPart() {
return new EssentiaImportBusPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable Item.TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_import_bus.desc"));
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_import_bus.hint"));
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.EssentiaLevelEmitterPart;
import java.util.List;
public class PartItemEssentiaLevelEmitter extends Item implements IPartItem<EssentiaLevelEmitterPart> {
public PartItemEssentiaLevelEmitter(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<EssentiaLevelEmitterPart> getPartClass() {
return EssentiaLevelEmitterPart.class;
}
@Override
public EssentiaLevelEmitterPart createPart() {
return new EssentiaLevelEmitterPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable Item.TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_level_emitter.desc"));
}
}
@@ -0,0 +1,48 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.EssentiaStorageBusPart;
import java.util.List;
public class PartItemEssentiaStorageBus extends Item implements IPartItem<EssentiaStorageBusPart> {
public static PartItemEssentiaStorageBus create() {
return new PartItemEssentiaStorageBus(new Item.Properties().stacksTo(64));
}
public PartItemEssentiaStorageBus(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<EssentiaStorageBusPart> getPartClass() {
return EssentiaStorageBusPart.class;
}
// 传 this:本类实现了 IPartItem<EssentiaStorageBusPart>
@Override
public EssentiaStorageBusPart createPart() {
return new EssentiaStorageBusPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable Item.TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_storage_bus.desc"));
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.items;
import appeng.api.parts.IPartItem;
import appeng.api.parts.PartHelper;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.item.context.UseOnContext;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.EssentiaTerminalPart;
import java.util.List;
public class PartItemEssentiaTerminal extends Item implements IPartItem<EssentiaTerminalPart> {
public PartItemEssentiaTerminal(Properties properties) {
super(properties);
}
@Override
public InteractionResult useOn(UseOnContext context) {
return PartHelper.usePartItem(context);
}
@Override
public Class<EssentiaTerminalPart> getPartClass() {
return EssentiaTerminalPart.class;
}
@Override
public EssentiaTerminalPart createPart() {
return new EssentiaTerminalPart(this);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.essentia_terminal.desc"));
}
}
@@ -0,0 +1,24 @@
package thaumicenergistics.common.items;
import appeng.items.parts.PartItem;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.common.parts.VisInterfacePart;
import java.util.List;
public class PartItemVisInterface extends PartItem<VisInterfacePart> {
public PartItemVisInterface(Properties properties) {
super(properties, VisInterfacePart.class, VisInterfacePart::new);
}
@Override
public void appendHoverText(ItemStack stack, @Nullable TooltipContext context,
List<Component> tooltip, TooltipFlag flag) {
super.appendHoverText(stack, context, tooltip, flag);
tooltip.add(Component.translatable("tooltip.thaumicenergistics.vis_interface.desc"));
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.items;
import java.util.function.DoubleSupplier;
import org.jetbrains.annotations.Nullable;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.BlockHitResult;
import appeng.helpers.WirelessTerminalMenuHost;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.menu.locator.ItemMenuHostLocator;
import appeng.menu.me.common.MEStorageMenu;
/**
* 无线源质终端物品。继承 AE2 的WirelessTerminalItem
* 右键使用时打开仅显示源质的 AE2 无线终端 GUI。
*/
public class WirelessEssentiaTerminalItem extends WirelessTerminalItem {
public WirelessEssentiaTerminalItem(DoubleSupplier powerCapacity, Properties props) {
super(powerCapacity, props);
}
@Override
public MenuType<?> getMenuType() {
return MEStorageMenu.WIRELESS_TYPE;
}
@Override
@Nullable
public WirelessTerminalMenuHost<?> getMenuHost(
Player player,
ItemMenuHostLocator locator,
@Nullable BlockHitResult hitResult) {
return new WirelessEssentiaTerminalMenuHost(
this,
player,
locator,
(p, subMenu) -> this.openFromInventory(p, locator, true));
}
}
@@ -0,0 +1,35 @@
package thaumicenergistics.common.items;
import java.util.function.BiConsumer;
import net.minecraft.world.entity.player.Player;
import appeng.api.util.KeyTypeSelection;
import appeng.helpers.WirelessTerminalMenuHost;
import appeng.menu.ISubMenu;
import appeng.menu.locator.ItemMenuHostLocator;
/**
* 无线源质终端的 MenuHost:继承 AE2 WirelessTerminalMenuHost,覆盖 getKeyTypeSelection()
* 仅允许源质类型(其余 WAP 连接/电力/存储复用父类)。
*/
public class WirelessEssentiaTerminalMenuHost
extends WirelessTerminalMenuHost<WirelessEssentiaTerminalItem> {
public WirelessEssentiaTerminalMenuHost(
WirelessEssentiaTerminalItem item,
Player player,
ItemMenuHostLocator locator,
BiConsumer<Player, ISubMenu> returnToMainMenu) {
super(item, player, locator, returnToMainMenu);
}
/**
* 仅允许源质类型,禁止物品和流体(与 EssentiaTerminalPart 一致)。
*/
@Override
public KeyTypeSelection getKeyTypeSelection() {
return new KeyTypeSelection(
() -> {},
keyType -> keyType == thaumicenergistics.common.integration.appeng.AEssentiaKeyType.INSTANCE);
}
}
@@ -0,0 +1,118 @@
package thaumicenergistics.common.network;
import net.minecraft.client.Minecraft;
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.resources.ResourceLocation;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerEssentiaCellWorkbench;
/**
* 客户端→服务端 网络包:源质元件工作台分区操作。
* 操作类型见 ACTION_* 常量;aspectA/aspectB 仅在对应操作使用(其余传 EMPTY)。
*/
public record CellWorkbenchC2SPacket(
int containerId,
int action,
ResourceLocation aspectA,
ResourceLocation aspectB
) implements CustomPacketPayload {
public static final int ACTION_REQUEST = 0;
public static final int ACTION_ADD = 1;
public static final int ACTION_REMOVE = 2;
public static final int ACTION_REPLACE = 3;
public static final int ACTION_CLEAR = 4;
public static final int ACTION_PARTITION_TO_CONTENTS = 5;
private static final ResourceLocation EMPTY = ResourceLocation.withDefaultNamespace("empty");
public static final Type<CellWorkbenchC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("cell_workbench_c2s"));
public static final StreamCodec<RegistryFriendlyByteBuf, CellWorkbenchC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, CellWorkbenchC2SPacket::containerId,
ByteBufCodecs.VAR_INT, CellWorkbenchC2SPacket::action,
ResourceLocation.STREAM_CODEC, CellWorkbenchC2SPacket::aspectA,
ResourceLocation.STREAM_CODEC, CellWorkbenchC2SPacket::aspectB,
CellWorkbenchC2SPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
// ===== 客户端静态发送(从当前打开的工作台菜单自动取 containerId =====
private static void send(int action, ResourceLocation a, ResourceLocation b) {
var player = Minecraft.getInstance().player;
if (player == null) return;
PacketDistributor.sendToServer(new CellWorkbenchC2SPacket(player.containerMenu.containerId, action, a, b));
}
public static void sendRequestPartitions() {
send(ACTION_REQUEST, EMPTY, EMPTY);
}
public static void sendAddAspect(ResourceLocation aspect) {
send(ACTION_ADD, aspect, EMPTY);
}
public static void sendRemoveAspect(ResourceLocation aspect) {
send(ACTION_REMOVE, aspect, EMPTY);
}
public static void sendReplaceAspect(ResourceLocation from, ResourceLocation to) {
send(ACTION_REPLACE, from, to);
}
public static void sendClear() {
send(ACTION_CLEAR, EMPTY, EMPTY);
}
public static void sendPartitionToContents() {
send(ACTION_PARTITION_TO_CONTENTS, EMPTY, EMPTY);
}
// ===== 服务端处理 =====
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof ContainerEssentiaCellWorkbench menu) {
boolean changed;
switch (this.action) {
case ACTION_REQUEST -> {
if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer sp) {
CellWorkbenchSyncPacket.sendToClient(sp, menu.getPartitionAspects());
}
return;
}
case ACTION_ADD -> changed = menu.addAspectToPartition(this.aspectA);
case ACTION_REMOVE -> changed = menu.removeAspectFromPartition(this.aspectA);
case ACTION_REPLACE -> changed = menu.replaceAspectInPartition(this.aspectA, this.aspectB);
case ACTION_CLEAR -> {
menu.clearPartitioning();
changed = true;
}
case ACTION_PARTITION_TO_CONTENTS -> {
menu.partitionToContents();
changed = true;
}
default -> changed = false;
}
if (changed) {
if (ctx.player() instanceof net.minecraft.server.level.ServerPlayer sp) {
CellWorkbenchSyncPacket.sendToClient(sp, menu.getPartitionAspects());
}
}
}
});
}
}
@@ -0,0 +1,64 @@
package thaumicenergistics.common.network;
import java.util.ArrayList;
import java.util.List;
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.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.GuiEssentiaCellWorkbench;
/**
* 服务端→客户端 网络包:同步源质元件工作台的分区列表。
*
* 客户端收到后更新 {@link GuiEssentiaCellWorkbench#updatePartitions},刷新 63 个分区格。
*/
public record CellWorkbenchSyncPacket(
List<ResourceLocation> partitions
) implements CustomPacketPayload {
public static final Type<CellWorkbenchSyncPacket> TYPE =
new Type<>(ThaumicEnergistics.id("cell_workbench_sync"));
public static final StreamCodec<RegistryFriendlyByteBuf, CellWorkbenchSyncPacket> STREAM_CODEC = StreamCodec.of(
(buf, pkt) -> {
buf.writeVarInt(pkt.partitions.size());
for (ResourceLocation rl : pkt.partitions) {
ResourceLocation.STREAM_CODEC.encode(buf, rl);
}
},
buf -> {
int n = buf.readVarInt();
List<ResourceLocation> list = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
list.add(ResourceLocation.STREAM_CODEC.decode(buf));
}
return new CellWorkbenchSyncPacket(list);
}
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
/** 服务端发送分区列表到指定玩家。 */
public static void sendToClient(ServerPlayer player, List<ResourceLocation> partitions) {
PacketDistributor.sendToPlayer(player, new CellWorkbenchSyncPacket(partitions));
}
/** 客户端处理:更新打开的工作台 GUI 分区格。 */
public void handleOnClient(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (Minecraft.getInstance().screen instanceof GuiEssentiaCellWorkbench gui) {
gui.updatePartitions(this.partitions);
}
});
}
}

Some files were not shown because too many files have changed in this diff Show More