提交
This commit is contained in:
@@ -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 }
|
||||
@@ -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
|
||||
Vendored
BIN
Binary file not shown.
+8
@@ -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
|
||||
|
||||
@@ -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.71875(16×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 新 API:addVertex + 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; }
|
||||
}
|
||||
+393
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+357
@@ -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); }
|
||||
}
|
||||
+159
@@ -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<>();
|
||||
}
|
||||
+19
@@ -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),步长18(42-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.10,bar区域不参与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.10(146,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 内部使用 centivis,1 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 解码。
|
||||
* 使用 WeakHashMap:golem 实体被 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 源质 ID(ResourceLocation,如 "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 源质 ID(ResourceLocation)
|
||||
* @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 反序列化)与 config(TCReflection 生成)的 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;
|
||||
}
|
||||
}
|
||||
+31
@@ -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());
|
||||
}
|
||||
}
|
||||
+123
@@ -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 替换分区中的 from;from 不存在返回 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
Reference in New Issue
Block a user