Compare commits

...
5 Commits
Author SHA1 Message Date
1820982382 2e9f1fd4e3 [全流程完成] 2026-08-17 15:18:53 +08:00
1820982382 835a37a0c7 [全流程完成] 2026-08-17 15:17:42 +08:00
1820982382 da04b67ef0 [全流程完成] 2026-08-17 15:15:33 +08:00
1820982382 13b981a916 [全流程完成] 2026-08-17 15:15:04 +08:00
1820982382 286fc2d2c3 修改gitignore 2026-08-17 15:14:30 +08:00
61 changed files with 1797 additions and 333 deletions
+4
View File
@@ -16,3 +16,7 @@ progress.md
/modules/thaumicenergistics-neo/存储体系方案.md
/modules/thaumicenergistics-neo/计划书-配方溯源.md
/.reasonix/
/.tmp_*/
__pycache__/
/Issue-改善物品方块扫描要素.md
/路径.md
+1 -1
View File
@@ -1,5 +1,5 @@
# Project
mod_version=2.0.1
mod_version=2.1.1
mod_group=thaumicenergistics
# Mod
Vendored
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -63,6 +63,8 @@ public final class ThaumicEnergistics {
thaumicenergistics.common.network.ThENetworking.init(modBus);
thaumicenergistics.common.blocks.ThEWrenchHandler.register();
modBus.addListener(this::registerCapabilities);
PartModels.registerModels(
@@ -147,7 +149,14 @@ public final class ThaumicEnergistics {
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.ESSENTIA_TERMINAL.get(),
(EssentiaTerminalMenu menu, Inventory inv, Component title)
-> new GuiEssentiaTerminal(menu, inv, title,
StyleManager.loadStyleDoc("/screens/terminals/terminal.json")));
event.register(ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get(),
(EssentiaTerminalMenu menu, Inventory inv, Component title)
-> new GuiEssentiaTerminal(menu, inv, title,
StyleManager.loadStyleDoc("/screens/terminals/wireless_terminal.json")));
event.register(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), GuiKnowledgeInscriber::new);
event.register(ModMenuTypes.DISTILLATION_ENCODER.get(), GuiDistillationPatternEncoder::new);
@@ -1,10 +1,12 @@
package thaumicenergistics.common.blocks;
import java.util.List;
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.InteractionHand;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
@@ -14,10 +16,8 @@ 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.level.storage.loot.LootParams;
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;
@@ -43,20 +43,16 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock
@Override
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
/**
* 谐振仓:通配吸力模式,不再需要安瓿瓶切换燃料类型。
* 放行默认交互(若后续添加 GUI 则走 getMenuProvider)。
*/
@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;
}
@@ -68,4 +64,12 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock
}
super.onRemove(state, level, pos, newState, moved);
}
/**
* 挖掘掉落方块自身(不导出方块实体 NBT)。
*/
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return List.of(new ItemStack(this));
}
}
@@ -1,13 +1,21 @@
package thaumicenergistics.common.blocks;
import java.util.List;
import net.minecraft.core.BlockPos;
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.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 net.minecraft.world.level.storage.loot.LootParams;
import net.minecraft.world.phys.BlockHitResult;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.common.tiles.TileInfusionProvider;
@@ -36,6 +44,24 @@ public abstract class ThEBaseEntityBlock extends Block implements EntityBlock {
return (BlockEntityTicker<T>) ticker;
}
/**
* AE2 扳手拆卸。WrenchHook 只处理 AEBaseBlockEntity,ThE 方块 BE 不走该路径,故在此自行拆卸并掉落。
*/
@Override
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos,
Player player, InteractionHand hand, BlockHitResult hit) {
// 扳手拆卸由 ThEWrenchHandler(RightClickBlock 事件)处理,这里不重复
return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
/**
* 挖掘掉落方块自身(不导出方块实体 NBT)。
*/
@Override
public List<ItemStack> getDrops(BlockState state, LootParams.Builder builder) {
return List.of(new ItemStack(this));
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
if (!state.is(newState.getBlock())) {
@@ -0,0 +1,67 @@
package thaumicenergistics.common.blocks;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
import thaumicenergistics.ThaumicEnergistics;
/**
* AE2 扳手拆卸处理。AE2 的 WrenchHook 只处理 AEBaseBlockEntity,ThE 方块 BE 不走该路径;
* 这里用与 WrenchHook 同级的 RightClickBlock 事件,在 Block.useItemOn 之前拦截扳手右键,
* 对 ThE 方块执行拆卸(掉落 + 移除)。
*/
public final class ThEWrenchHandler {
private ThEWrenchHandler() {
}
public static void register() {
NeoForge.EVENT_BUS.register(ThEWrenchHandler.class);
}
@SubscribeEvent
public static void onRightClickBlock(PlayerInteractEvent.RightClickBlock event) {
if (event.isCanceled() || event.getHand() != InteractionHand.MAIN_HAND) {
return;
}
var player = event.getEntity();
if (player.isSpectator()) {
return;
}
var stack = player.getItemInHand(event.getHand());
if (!player.isShiftKeyDown() || !stack.is(appeng.datagen.providers.tags.ConventionTags.WRENCH)) {
return;
}
var level = event.getLevel();
BlockPos pos = event.getPos();
BlockState state = level.getBlockState(pos);
// 只处理 ThE 方块(按命名空间判断,兼容未继承 ThETileBase 的工作台等)
if (!ThaumicEnergistics.MODID.equals(state.getBlock().builtInRegistryHolder().key().location().getNamespace())) {
return;
}
BlockEntity be = level.getBlockEntity(pos);
if (be == null) {
return;
}
if (!level.isClientSide) {
// ThE 方块掉落即自身(挖掘走 getDrops 覆写;扳手拆卸直接给方块物品,避免 loot table 为空)
player.getInventory().placeItemBackInInventory(new ItemStack(be.getBlockState().getBlock()));
level.removeBlock(pos, false);
} else {
level.playSound(player, pos, SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 0.7F, 1.0F);
}
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
}
}
@@ -13,7 +13,7 @@ import net.neoforged.neoforge.items.SlotItemHandler;
import thaumcraft.api.IVisDiscountGear;
import thaumcraft.api.IWarpingGear;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.init.ModMenuTypes;
@@ -160,16 +160,12 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
}
});
// ===== 4. 3×7 配方槽(只读展示) =====
// ===== 4. 3×7 配方槽(只读展示,AE2 式防整理) =====
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; }
});
addSlot(new ReadOnlySlot(machineHandler, invIdx,
PATTERN_X + c * 18, PATTERN_Y + r * 18));
}
// ===== 5. 4张AE2速度卡 =====
@@ -182,11 +178,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
});
}
// ===== 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; }
});
// ===== 6. 目标产物输出槽(只读展示,AE2 式防整理) =====
addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y));
// ===== 7. 4个护甲折扣槽 =====
for (int i = 0; i < ARMOR_COUNT; i++) {
@@ -202,9 +195,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
@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);
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem)
return eqItem.getEquipmentSlot() == eq;
return false;
}
@Override public void setChanged() {
super.setChanged();
@@ -220,13 +213,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
this(id, inv, resolveTile(inv, buf));
}
/** 临时诊断:确认客户端能否拿到 tile(定位后删除) */
private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) {
var pos = buf.readBlockPos();
var be = inv.player.level().getBlockEntity(pos);
ThaumicEnergistics.LOG.info("[AA-DEBUG] client ctor: pos={}, be={}",
pos, be == null ? "null" : be.getClass().getSimpleName());
return (TileArcaneAssembler) be;
return (TileArcaneAssembler) inv.player.level().getBlockEntity(pos);
}
private ItemStackHandler wrapMachineInventory() {
@@ -33,9 +33,11 @@ import appeng.me.storage.LinkStatusRespectingInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.wands.WandCap;
import net.minecraft.world.item.crafting.Ingredient;
import thaumcraft.common.items.TCFunctionalItems;
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
import thaumcraft.common.vis.TCVisHelper;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
@@ -55,6 +57,34 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
*/
private boolean isCraftingResultBeingTaken = false;
/** vis 恢复检测节流:每 10 tick(0.5 秒)检查一次,避免持续遍历配方。 */
private static final int VIS_RECOVERY_CHECK_INTERVAL = 10;
private int visRecoveryTickCounter = 0;
@Override
public void broadcastChanges() {
super.broadcastChanges();
// vis 充能恢复后自动重算结果槽:结果槽空 + 奥术配方有效 + vis 已足够 → 重新显示产物。
// 节流:仅"待恢复"状态每 10 tick 检查一次(findMatchingArcaneRecipe 只在满足时执行)
var player = getPlayerInventory().player;
if (player.level().isClientSide() || !hasValidRecipe || !resultSlot.getItem().isEmpty()) {
return;
}
if (++visRecoveryTickCounter < VIS_RECOVERY_CHECK_INTERVAL) {
return;
}
visRecoveryTickCounter = 0;
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
if (!arcaneResult.isEmpty()) {
AspectList aspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
if (aspects != null && hasEnoughVis(aspects)) {
resultSlot.setRequiredAspects(aspects);
resultSlot.setDisplayedCraftingOutput(arcaneResult);
}
}
}
private final ISegmentedInventory craftingInventoryHost;
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
private final ArcaneCraftingResultSlot resultSlot;
@@ -76,9 +106,9 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
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.resultSlot = new ArcaneCraftingResultSlot(
getPlayerInventory().player, this, craftingGridInv, craftingGridInv,
linkStatusInventory, this.getEnergySource(), this.getActionSource());
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
@@ -88,7 +118,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return 1;
}
@Override
public boolean mayPlace(ItemStack stack) {
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
@@ -145,6 +174,26 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
private static final int CENTIVIS = 100;
/**
* 复刻 TC port {@code WandCastingItem.consumptionModifier}:法杖 cap 折扣 − 玩家装备/饰品折扣 − 法杖折扣。
* 仅用于显示(GUI 所需 vis),实际扣费由 consumeVisCost 内部同公式处理。
*/
private float getConsumptionModifier(Aspect aspect, boolean crafting) {
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0);
if (wandStack.isEmpty() || !(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return 1.0f;
}
float modifier = wand.consumptionModifier(wandStack, getPlayerInventory().player, aspect, crafting);
return modifier;
}
/** 折扣后的 vis 成本(GUI 显示用)。 */
private int getDiscountedVisCost(Aspect aspect, int rawCost) {
if (rawCost <= 0 || aspect == null) return 0;
return Math.max(0, Math.round(rawCost * getConsumptionModifier(aspect, true)));
}
private boolean hasEnoughVis(AspectList cost) {
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
if (cost == null || cost.isEmpty()) {
@@ -172,7 +221,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate);
var player = getPlayerInventory().player;
var level = player.level();
@@ -183,27 +231,20 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
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());
@@ -212,10 +253,8 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
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);
@@ -225,9 +264,7 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
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;
@@ -235,32 +272,27 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
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());
// 存折扣后数值(GUI 显示实际所需 vis,含法杖 + 装备 + 饰品减免)
sb.append(entry.getKey().getTag()).append("=")
.append(getDiscountedVisCost(entry.getKey(), 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)");
}
}
@@ -24,6 +24,7 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.common.research.TCResearchManager;
import thaumcraft.common.research.ThaumometerScanManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
@@ -98,20 +99,10 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
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 ReadOnlySlot(aspectInventory, i, SLOT_ASPECTS_X, y));
}
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; }
});
addSlot(new ReadOnlySlot(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y));
if (encoder != null) {
// 源物品槽:ghost 模板(背包拖入不消耗——实际物品还在手里;空手点击清空;JEI 拖拽设置模板)
@@ -1,20 +0,0 @@
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); }
}
@@ -25,6 +25,7 @@ import thaumcraft.api.crafting.ShapedArcaneRecipe;
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.common.container.slot.GhostSlot;
import thaumicenergistics.common.container.slot.ReadOnlySlot;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
@@ -121,12 +122,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
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; }
});
addSlot(new ReadOnlySlot(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y));
saveStateSlot = addDataSlot(DataSlot.standalone());
saveStateSlot.set(0);
@@ -0,0 +1,216 @@
package thaumicenergistics.common.container;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
import appeng.api.config.Actionable;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.networking.security.IActionSource;
import appeng.api.stacks.AEKey;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumcraft.common.items.JarBlockItem;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
* 源质容器 ↔ ME 网络通用存取逻辑(服务端)。
* <p>
* 与具体菜单解耦:调用方(源质终端覆写、或 ME 终端 mixin)只需提供
* storage / energySource / actionSource / player / 物品栈,本类完成
* 罐子与安瓿的装罐、存入网络操作。
* <ul>
* <li>取出(装罐):网络有源质 → 装入手持空罐(有多少装多少,最多 64)/ 空安瓿(固定 8)。</li>
* <li>存入:手持/点击的罐子或安瓿 → 源质注入网络;罐子留空罐,安瓿返回空 glass_phial(不消失)。</li>
* </ul>
*/
public final class EssentiaFillHelper {
private EssentiaFillHelper() {}
/** 玻璃安瓿固定容量(TC 原版:8 源质/支)。 */
public static final int PHIAL_CAPACITY = 8;
/** 罐子容量(TC 原版:64 源质/罐)。 */
public static final int JAR_CAPACITY = 64;
/**
* 右键网络源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。
*
* @return true = 处理成功(装罐/装安瓿),false = 无法处理
*/
public static boolean fillContainerFromGrid(MEStorage storage, IEnergySource energySource,
IActionSource actionSource, Player player,
ItemStack carried, ResourceLocation aspectId) {
if (carried.isEmpty() || aspectId == null) {
return false;
}
AEKey key = AEssentiaKey.of(aspectId);
if (key == null) {
return false;
}
long amountInGrid = storage.getAvailableStacks().get(key);
if (amountInGrid <= 0) {
return false;
}
// 玻璃安瓿:消耗空安瓿 → 得到装满安瓿(固定 8 源质)
if (carried.getItem() == BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial"))) {
if (amountInGrid < PHIAL_CAPACITY) {
return false;
}
long extracted = storage.extract(key, PHIAL_CAPACITY, Actionable.MODULATE, actionSource);
if (extracted < PHIAL_CAPACITY) {
if (extracted > 0) {
storage.insert(key, extracted, Actionable.MODULATE, actionSource);
}
return false;
}
carried.shrink(1);
// 得到装满安瓿并放入背包(装好后自动放入背包)
ItemStack filled = filledPhialStack(aspectId.getPath());
if (!player.getInventory().add(filled)) {
player.drop(filled, false);
}
return true;
}
// 罐子(IEssentiaContainerItem):每次只装 1 个空罐(手堆叠 -1,背包得 1 满罐)
if (!(carried.getItem() instanceof IEssentiaContainerItem container)) {
return false;
}
AspectList existing = container.getAspects(carried);
if (existing != null && !existing.isEmpty()) {
return false; // 罐子非空,不能装
}
// 部分取出:网络有多少装多少(最多 JAR_CAPACITY 64);不足 64 也能装,不再整组拒绝
long toExtract = Math.min(amountInGrid, JAR_CAPACITY);
long extracted = storage.extract(key, toExtract, Actionable.MODULATE, actionSource);
if (extracted <= 0) {
return false;
}
Aspect aspect = Aspect.getAspect(aspectId.getPath());
if (aspect == null) {
storage.insert(key, extracted, Actionable.MODULATE, actionSource);
return false;
}
// 生成 1 个满罐:必须先 copy(在 shrink 之前),因为 count==1 的堆叠 shrink 后
// 会变成空堆叠,ItemStack.copy() 对空堆叠直接返回全局 EMPTY 单例,setCount(1)
// 会污染该常量产生"空气幽灵堆叠",罐子因此消失。copyWithCount(1) 生成独立副本,
// setAspects 只影响这一个,不影响原堆叠。
ItemStack filledJar = carried.copyWithCount(1);
// 手堆叠 -1
carried.shrink(1);
AspectList newAspects = new AspectList();
newAspects.add(aspect, (int) extracted);
container.setAspects(filledJar, newAspects);
if (!player.getInventory().add(filledJar)) {
player.drop(filledJar, false);
}
return true;
}
/**
* 把源质容器物品中的源质注入 ME 原生存储,返回处理后的物品栈。
* 堆叠中每个容器的源质相同(NBT 共享),按"单个量 × 堆叠数"一次注入;
* 全部注入成功才消耗容器,否则保留原堆叠,避免源质凭空消失。
* <p>
* 罐子抽干后留空罐(可重复使用);安瓿抽干后返回空 glass_phial(不消失)。
*
* @return null = 非源质容器(未处理,调用方应走原逻辑);
* 原 stack = 未注入(网络满/能量不足)或部分处理保留;
* 空罐 = 罐子全部抽干(可重复使用);
* glass_phial 空安瓿 = 安瓿全部抽干(返回空安瓿,不再消耗掉)
*/
@Nullable
public static ItemStack extractContainerContent(MEStorage storage, IEnergySource energySource,
IActionSource actionSource, ItemStack stack) {
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) {
return null;
}
int count = stack.getCount();
if (count <= 0) {
return stack;
}
AspectList aspects = container.getAspects(stack);
if (aspects == null || aspects.isEmpty()) {
return stack;
}
// 阶段1:预检(SIMULATE 无副作用)——网络必须能完整容纳全部源质才继续;
// 任一项放不下则整个堆叠原样保留,避免"部分注入后罐子被消耗、剩余源质凭空消失"。
Aspect[] aspectArr = aspects.getAspects();
ResourceLocation[] ids = new ResourceLocation[aspectArr.length];
long[] totals = new long[aspectArr.length];
int n = 0;
for (Aspect aspect : aspectArr) {
if (aspect == null) {
continue;
}
int perItem = aspects.getAmount(aspect);
if (perItem <= 0) {
continue;
}
ResourceLocation aspectId = TCReflection.getAspectId(aspect);
if (aspectId == null) {
continue;
}
long total = (long) perItem * count;
if (storage.insert(AEssentiaKey.of(aspectId), total,
Actionable.SIMULATE, actionSource) < total) {
return stack; // 网络容量不足:完全不注入,罐子/安瓿原样保留
}
ids[n] = aspectId;
totals[n] = total;
n++;
}
if (n == 0) {
return stack;
}
// 阶段2:执行注入(预检已保证容量;若能量不足导致未完全注入,则回滚本次及此前
// 已注入的量并保留罐子,保证网络数量与罐子状态一致——不丢源质也不复制源质)
for (int i = 0; i < n; i++) {
long injected = StorageHelper.poweredInsert(
energySource, storage, AEssentiaKey.of(ids[i]), totals[i], actionSource);
if (injected < totals[i]) {
if (injected > 0) {
storage.insert(AEssentiaKey.of(ids[i]), injected,
Actionable.MODULATE, actionSource);
}
for (int j = 0; j < i; j++) {
storage.insert(AEssentiaKey.of(ids[j]), totals[j],
Actionable.MODULATE, actionSource);
}
return stack;
}
}
// 全部注入成功则消耗整个堆叠(罐子留空罐、安瓿返回空 glass_phial)
if (stack.getItem() instanceof JarBlockItem) {
return new ItemStack(stack.getItem());
}
// 安瓿:返回空安瓿(glass_phial),不直接消失
ItemStack emptyPhial = new ItemStack(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")), count);
return emptyPhial;
}
/** 生成装满的安瓿(phial_of_essentia_&lt;tag&gt;),与 TC JarBlock.filledPhialStack 一致。 */
private static ItemStack filledPhialStack(String aspectTag) {
ResourceLocation id = ResourceLocation.fromNamespaceAndPath(
"thaumcraft", "phial_of_essentia_" + aspectTag);
net.minecraft.world.item.Item item = BuiltInRegistries.ITEM.getOptional(id)
.orElse(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")));
return new ItemStack(item);
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.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.resources.ResourceLocation;
import appeng.api.storage.ITerminalHost;
import appeng.menu.me.common.MEStorageMenu;
/**
* 源质终端的菜单(有线端与无线端共用)。
* <p>
* 继承 AE2 原版 {@link MEStorageMenu} 的全部普通物品行为(左键/Shift+点击/拖拽均按普通物品处理)。
* 源质的存入与取出统一由右键交互承担:
* <ul>
* <li>手持装有源质的罐子/安瓿 + 右键任意处 → 源质存入网络({@link EssentiaFillHelper#extractContainerContent})。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出装罐({@link #fillContainerFromGrid})。</li>
* </ul>
*/
public class EssentiaTerminalMenu extends MEStorageMenu {
public EssentiaTerminalMenu(MenuType<?> menuType, int id, Inventory ip, ITerminalHost host) {
super(menuType, id, ip, host);
}
/**
* 右键终端源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。
* 具体逻辑见 {@link EssentiaFillHelper#fillContainerFromGrid}。
*
* @param aspectId 源质 aspect 的 ResourceLocation
* @return true = 处理成功(装罐/装安瓿),false = 无法处理
*/
public boolean fillContainerFromGrid(ResourceLocation aspectId) {
if (isClientSide()) {
return false;
}
var player = getPlayerInventory().player;
var carried = getCarried();
return EssentiaFillHelper.fillContainerFromGrid(
this.storage, this.energySource, getActionSource(), player, carried, aspectId);
}
}
@@ -7,9 +7,10 @@ import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ContainerData;
import thaumicenergistics.ThaumicEnergistics;
import org.anti_ad.mc.ipn.api.IPNIgnore;
/**
* 奥术装配器 GUI。
* 奥术装配器 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
*
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
* 第1块 主矩形:(0,0) → 176×197
@@ -20,6 +21,7 @@ import thaumicenergistics.ThaumicEnergistics;
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
*/
@IPNIgnore
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
@@ -11,17 +11,13 @@ import appeng.client.gui.style.ScreenStyle;
import java.util.Map;
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
import org.anti_ad.mc.ipn.api.IPNIgnore;
/** 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行
};
/**
* 奥术合成终端 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
*/
@IPNIgnore
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
private static final int ICON_SIZE = 14;
@@ -30,9 +26,6 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
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);
@@ -42,22 +35,17 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
Map<String, Integer> aspectMap = this.menu.getParsedAspects();
if (aspectMap.isEmpty()) return;
// 获取合成结果槽的 Y 位置作为锚点(槽位置不变,首次计算后缓存,避免每帧遍历)
int anchorY;
if (cachedAnchorY < 0) {
// 锚点取合成结果槽的 Y:圆环紧贴结果槽左侧,窗口缩放时随槽位一起移动
int anchorY = -1;
for (var slot : this.menu.slots) {
// CRAFTING_RESULT 槽在 style JSON 中 bottom:140
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
cachedAnchorY = slot.y;
anchorY = slot.y;
break;
}
}
// 若找不到锚点则回退到硬编码值
if (cachedAnchorY < 0) cachedAnchorY = 95;
}
anchorY = cachedAnchorY;
if (anchorY < 0) anchorY = 95;
// 要素圆圈 Y = 锚点 + 偏移(-16/1/17,对应纹理 95/112/128)
// 两列三行:左列 x=10,右列 x=80;行偏移对应三行圆环
int[] aspectOffsets = { -16, 1, 17 };
int index = 0;
@@ -1,15 +0,0 @@
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,118 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.style.ScreenStyle;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket;
import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket;
/**
* 源质终端 GUI:在 AE2 通用终端基础上扩展右键交互。
* <ul>
* <li>Shift+右键玩家背包格:格内是源质容器 → 源质存入网络(空容器留原格)。</li>
* <li>右键玩家物品栏/背包槽 → 放行(AE2 原版物品交互)。</li>
* <li>右键网络区域(源质条目或空白),手持装有源质的罐子/安瓿 → 源质存入 ME 网络(空容器回背包)。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出源质装罐/装安瓿。</li>
* <li>左键仍是正常物品操作(不受影响)。</li>
* </ul>
*/
public class GuiEssentiaTerminal extends MEStorageScreen<EssentiaTerminalMenu> {
public GuiEssentiaTerminal(EssentiaTerminalMenu menu, Inventory playerInventory,
Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
@Override
public boolean mouseClicked(double xCoord, double yCoord, int btn) {
if (btn == 1) {
var carried = this.menu.getCarried();
boolean shiftDown = net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.hasShiftDown();
// 源质物品来源:优先菜单 carried;为空时回退玩家主手物品
ItemStack sourceStack = !carried.isEmpty() ? carried
: this.menu.getPlayerInventory().player.getMainHandItem();
boolean isContainer = isSourceContainer(sourceStack);
boolean containerHasEssentia = isContainer && getContainerEssentia(sourceStack) != null;
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) {
var slotStack = this.hoveredSlot.getItem();
if (slotStack.getItem() instanceof IEssentiaContainerItem) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
this.menu.containerId, this.hoveredSlot.getSlotIndex(), slotStack));
return true;
}
// 非源质容器 → 放行(AE2 原版 Shift+右键快速移动)
return super.mouseClicked(xCoord, yCoord, btn);
}
// 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互)
if (!shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) {
return super.mouseClicked(xCoord, yCoord, btn);
}
// Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当普通物品放入网络
if (shiftDown && isContainer) {
if (containerHasEssentia) {
this.menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
this.menu.containerId, sourceStack));
}
return true;
}
// 普通右键网络区域:手持满容器 → 存入源质
if (containerHasEssentia) {
this.menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
this.menu.containerId, sourceStack));
return true;
}
// 手持空容器:右键源质条目 → 取出装罐;右键网络其他 → 拦截(防被放入网络)
if (isContainer) {
if (this.hoveredSlot instanceof appeng.client.gui.me.common.RepoSlot repoSlot) {
var entry = repoSlot.getEntry();
if (entry != null && entry.getWhat() instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey key) {
PacketDistributor.sendToServer(EssentiaTerminalFillC2SPacket.create(
this.menu.containerId, key.getId(), sourceStack));
return true;
}
}
return true;
}
}
return super.mouseClicked(xCoord, yCoord, btn);
}
/** 源质容器:实现 IEssentiaContainerItem 的物品,或空玻璃安瓿(glass_phial 不实现该接口但可装罐)。 */
private static boolean isSourceContainer(ItemStack stack) {
if (stack.getItem() instanceof IEssentiaContainerItem) {
return true;
}
return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")));
}
/** 若手持物品是源质容器且非空,返回其 AspectList;否则返回 null。 */
@Nullable
private static AspectList getContainerEssentia(ItemStack stack) {
if (stack.getItem() instanceof IEssentiaContainerItem container) {
AspectList aspects = container.getAspects(stack);
if (aspects != null && !aspects.isEmpty()) {
return aspects;
}
}
return null;
}
}
@@ -0,0 +1,22 @@
package thaumicenergistics.common.container;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.storage.MEStorage;
/**
* 由 {@code MixinMEStorageMenuAccessors} 注入到 {@code MEStorageMenu} 的访问器接口。
* <p>
* 编译期接口存在、运行期目标类实现,调用方(如网络包处理)通过
* {@code instanceof ThEEssentiaMenuAccess} 安全访问 ME 存储与能量源。
* <p>
* 注意:必须放在非 mixin 包(mixin 包 {@code thaumicenergistics.mixins.*} 的类
* 禁止被普通类直接引用)。
*/
public interface ThEEssentiaMenuAccess {
/** ME 存储(对应 MEStorageMenu.storage)。 */
MEStorage thE_getStorage();
/** 能量源(对应 MEStorageMenu.energySource)。 */
IEnergySource thE_getEnergySource();
}
@@ -4,37 +4,44 @@ import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import appeng.api.inventories.InternalInventory;
import appeng.menu.slot.AppEngCraftingSlot;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.networking.security.IActionSource;
import appeng.api.storage.MEStorage;
import appeng.helpers.ICraftingGridMenu;
import appeng.helpers.InventoryAction;
import appeng.menu.slot.CraftingTermSlot;
import appeng.util.inv.PlayerInternalInventory;
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。
* 奥术合成结果槽:继承 AE2 CraftingTermSlot 使客户端识别(shift/右键/space 合成自动生效),
* 并保留 vis 消耗检查(mayPickup)与取走时 vis 扣除 + 自动补货(onTake)。
*/
public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
public class ArcaneCraftingResultSlot extends CraftingTermSlot {
/** 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;
private final MEStorage storage;
private final IEnergySource energySrc;
private final IActionSource mySrc;
private final InternalInventory craftingGrid;
public ArcaneCraftingResultSlot(Player player, InternalInventory craftingGridInv) {
super(player, craftingGridInv);
}
public void setContainer(ContainerArcaneCraftingTerminal container) {
public ArcaneCraftingResultSlot(Player player, ContainerArcaneCraftingTerminal container,
InternalInventory craftingGridInv, InternalInventory secondMatrix,
MEStorage storage, IEnergySource energySrc, IActionSource mySrc) {
super(player, mySrc, energySrc, storage, craftingGridInv, secondMatrix, (ICraftingGridMenu) container);
this.container = container;
}
public void setRefreshCallback(Runnable callback) {
this.refreshCallback = callback;
this.storage = storage;
this.energySrc = energySrc;
this.mySrc = mySrc;
this.craftingGrid = craftingGridInv;
}
public void setRequiredAspects(AspectList aspects) {
@@ -47,40 +54,177 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
@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)) {
// AE2 原版:结果槽不可 vanilla 点按取走,所有取走走 doClick(含 vis 检查)
return false;
}
@Override
public void doClick(InventoryAction action, Player who) {
// vis 检查:奥术配方需法杖有足够 vis,否则不合成
if (requiredAspects != null && !requiredAspects.isEmpty()) {
if (wandStack.isEmpty()) {
return;
}
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return;
}
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
return wand.consumeVisCost(wandStack, player, centivisCost, false, true);
if (!wand.consumeVisCost(wandStack, who, centivisCost, false, true)) {
return;
}
}
// 奥术配方(非 vanilla CRAFTING)不走 AE2 craftItem(findRecipe 只认 vanilla)
// 走自定义合成:从合成格读配方 → 产物 + 消耗材料 + 从网络补货
if (container != null && container.hasValidRecipe) {
doArcaneCraft(action, who);
return;
}
// vanilla 配方:严格 AE2(从网络提取材料填合成格)
super.doClick(action, who);
// 消耗 vis(实际扣除)
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, who, centivisCost, true, true);
}
}
}
/** 奥术配方合成:从合成格读 Thaumcraft 奥术配方 → 产物 + 消耗材料 + 从网络补货。 */
private void doArcaneCraft(InventoryAction action, Player who) {
if (this.getItem().isEmpty() || isRemote()) {
return;
}
var grid = craftingGrid;
var level = who.level();
if (level.isClientSide()) return;
// 用 Thaumcraft 奥术配方匹配(取配方对象以支持 retainIngredient 保留原料)
var craftContainer = grid.toContainer();
thaumcraft.api.crafting.IArcaneRecipe arcaneRecipe = thaumcraft.common.lib.crafting.ThaumcraftCraftingManager
.findMatchingArcaneRecipeEntry(craftContainer, who);
if (arcaneRecipe == null) {
return;
}
ItemStack arcaneResult = arcaneRecipe.getCraftingResult(craftContainer);
if (arcaneResult.isEmpty()) {
return;
}
// 保留原料(retainIngredient,如元始珍珠):getRemainingItems 返回非空栈的槽位不消耗。
// 对应 TC 奥术工作台行为——珍珠作为"催化"留在合成格,仅消耗构材与虚空金属锭。
net.minecraft.core.NonNullList<net.minecraft.world.item.ItemStack> remainders =
arcaneRecipe.getRemainingItems(craftContainer);
// shift=整组到背包、普通=1 个到手
int maxTimesToCraft;
InternalInventory target;
if (action == InventoryAction.CRAFT_SHIFT || action == InventoryAction.CRAFT_ALL) {
target = new appeng.util.inv.PlayerInternalInventory(who.getInventory());
maxTimesToCraft = (int) Math.floor((double) arcaneResult.getMaxStackSize() / (double) arcaneResult.getCount());
if (action == InventoryAction.CRAFT_ALL) {
maxTimesToCraft *= net.minecraft.world.entity.player.Inventory.INVENTORY_SIZE;
}
} else {
target = new appeng.util.inv.CarriedItemInventory(getMenu());
maxTimesToCraft = 1;
}
int crafted = 0;
for (int x = 0; x < maxTimesToCraft; x++) {
// 每轮 vis 检查 + 实际扣减(consume=true):法杖 vis 不足则停
if (requiredAspects != null && !requiredAspects.isEmpty()) {
if (wandStack.isEmpty()) break;
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) break;
AspectList centivisCost = new AspectList();
for (Aspect aspect : requiredAspects.aspects()) {
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
}
boolean ok = wand.consumeVisCost(wandStack, who, centivisCost, true, true);
if (!ok) {
break;
}
}
// 目标满或配方失效则停
if (!target.simulateAdd(arcaneResult).isEmpty()) {
break;
}
// 合成前检查奥术配方仍匹配
ItemStack currentResult = thaumcraft.common.lib.crafting.ThaumcraftCraftingManager
.findMatchingArcaneRecipe(grid.toContainer(), who);
if (currentResult.isEmpty()) {
break;
}
// 记录消耗前合成格(首次记录,后续每轮消耗后补货)
var beforeItems = new java.util.HashMap<Integer, ItemStack>();
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) beforeItems.put(i, s.copy());
}
// 消耗合成格材料(每格 1 个);保留原料槽(remainders 非空,如元始珍珠)不消耗
for (int i = 0; i < grid.size(); i++) {
if (!grid.getStackInSlot(i).isEmpty() && remainders.get(i).isEmpty()) {
grid.extractItem(i, 1, false);
}
}
// 从网络补货(对齐 AE2 终端)
if (storage != null && energySrc != null && mySrc != null) {
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(
energySrc, storage, key, needed, mySrc);
if (extracted > 0) {
grid.setItemDirect(slot, beforeItem.copyWithCount(
(int) (currentItem.getCount() + extracted)));
}
}
}
}
}
// 产物入目标
var extra = target.addItems(arcaneResult.copy());
if (!extra.isEmpty()) {
appeng.util.Platform.spawnDrops(who.level(), who.blockPosition(), java.util.List.of(extra));
break;
}
crafted++;
}
// 触发容器刷新(重算结果槽)
container.slotsChanged(grid.toContainer());
}
@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 {
// 记录消耗前各合成格物品,取走后从 ME 存储自动补回(AE2 终端行为)
java.util.Map<Integer, ItemStack> beforeItems = new java.util.HashMap<>();
var grid = craftingGrid;
for (int i = 0; i < grid.size(); i++) {
var s = grid.getStackInSlot(i);
if (!s.isEmpty()) beforeItems.put(i, s.copy());
}
super.onTake(player, stack);
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
@@ -93,28 +237,22 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
}
}
// 从 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();
// 自动补货:合成格材料被消耗后从 ME 存储补回
if (!beforeItems.isEmpty()) {
if (storage != null && energySrc != null && mySrc != null) {
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);
energySrc, storage, key, needed, mySrc);
if (extracted > 0) {
var toPlace = beforeItem.copyWithCount((int) (currentItem.getCount() + extracted));
grid.setItemDirect(slot, toPlace);
grid.setItemDirect(slot, beforeItem.copyWithCount(
(int) (currentItem.getCount() + extracted)));
}
}
}
@@ -126,8 +264,6 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
container.setCraftingResultBeingTaken(false);
}
}
if (refreshCallback != null) {
refreshCallback.run();
}
}
}
@@ -0,0 +1,58 @@
package thaumicenergistics.common.container.slot;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler;
/**
* 只读展示槽:显示 IItemHandler 中真实物品,但禁止玩家/整理 mod 交互。
* 与 AE2 ClientReadOnlySlot 等效(保留 inventory 用于渲染,mayPlace/mayPickup/set/remove 空操作)。
* 用于玩家不交互的纯展示槽(装配器的预存配方槽、目标产物槽)。
*/
public class ReadOnlySlot extends Slot {
/** 假 inventory 占位:阻止整理 mod 通过 slot 的 inventory/index 交互。 */
private static final Container EMPTY_INVENTORY = new SimpleContainer(0);
private final IItemHandler handler;
private final int handlerIndex;
public ReadOnlySlot(IItemHandler handler, int index, int xPosition, int yPosition) {
super(EMPTY_INVENTORY, 0, xPosition, yPosition);
this.handler = handler;
this.handlerIndex = index;
}
@Override
public ItemStack getItem() {
// 读取真实 inventory 用于渲染显示;handlerIndex 是 handler 内索引(Slot.index 是菜单槽序号,勿用)
return handler.getStackInSlot(handlerIndex);
}
@Override
public boolean mayPlace(ItemStack stack) {
return false;
}
@Override
public final void set(ItemStack stack) {
}
@Override
public int getMaxStackSize() {
return 0;
}
@Override
public ItemStack remove(int amount) {
return ItemStack.EMPTY;
}
@Override
public boolean mayPickup(Player player) {
return false;
}
}
@@ -128,7 +128,7 @@ public final class RecipeRegistration {
"lBl",
"QPQ",
'Q', ae("quartz_glass"),
'l', new ItemStack(Items.LAPIS_LAZULI, 4),
'l', new ItemStack(Items.LAPIS_LAZULI, 1),
'B', tc("zombie_brain"),
'P', ae("calculation_processor"));
@@ -138,7 +138,7 @@ public final class RecipeRegistration {
"qWq",
"FqA",
'A', tc("air_shard"),
'q', new ItemStack(Items.QUARTZ, 4),
'q', new ItemStack(Items.QUARTZ, 1),
'F', tc("fire_shard"),
'W', ae("certus_quartz_wrench"));
@@ -156,22 +156,23 @@ public final class RecipeRegistration {
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",
// 注意:TC ShapedArcaneRecipe 要求模式为矩形(各行等长),删 J 后必须补空格占位
"JD ",
"IFI",
" ",
'J', tc("warded_jar"),
'J', tc("jar_of_essentia"),
'D', stack(ModItems.DIFFUSION_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'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",
"JC ",
"IFI",
" ",
'J', tc("warded_jar"),
'J', tc("jar_of_essentia"),
'C', stack(ModItems.COALESCENCE_CORE),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'F', tc("essentia_filter"));
ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS),
@@ -182,7 +183,7 @@ public final class RecipeRegistration {
'D', stack(ModItems.DIFFUSION_CORE),
'C', stack(ModItems.COALESCENCE_CORE),
'F', tc("essentia_filter"),
'I', new ItemStack(Items.IRON_INGOT, 2),
'I', new ItemStack(Items.IRON_INGOT, 1),
'G', ae("quartz_glass"));
KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
@@ -190,7 +191,7 @@ public final class RecipeRegistration {
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'I', new ItemStack(Items.IRON_INGOT, 1),
'L', MONITORS,
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
@@ -200,7 +201,7 @@ public final class RecipeRegistration {
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'I', new ItemStack(Items.IRON_INGOT, 1),
'L', MONITORS,
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
@@ -252,7 +253,7 @@ public final class RecipeRegistration {
// 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"),
tc("balanced_shard"),
ae("me_p2p_tunnel"));
// 7. 原质振荡室
@@ -293,27 +294,38 @@ public final class RecipeRegistration {
tc("salis_mundus"));
// 3. 奥术装配器
// 权杖要求:6 个原质要素值全满(满充能)。createFullVisScepter() 生成"满充能"预期栈,
// TC port 的 InfusionRecipe.hasExpectedComponents 会逐键比对 custom_data——
// 预期键必须全部存在于实物且数值相等 → 未满充能的权杖无法匹配。
// 组件数:8 个(1.7.10 原版一致)。实测 9 组件(含世界盐)注魔祭坛无法启动——
// TC 自身所有注魔配方组件数上限就是 8。
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("balanced_shard"),
tc("water_shard"),
tc("earth_shard"),
tc("air_shard"),
tc("salis_mundus"),
createFullVisScepter());
}
/**
* 生成"满充能"银木神秘权杖作为注魔配方的预期组件。
*
* ⚠ 关键:不要调用 TCVisHelper.setWandParts()!玩家实际合成的权杖(ArcaneSceptreRecipe →
* WandCastingItem.createWandFor)是纯物品,custom_data 只有充能时写入的 tc_vis_* 键,
* 没有 tc_wand_rod/tc_wand_cap。若在预期侧写入这些键,hasExpectedComponents 逐键比对时
* 会因实物缺少而永不匹配——满充能权杖也放不进去。
*
* getWandMaxVis 用 fallbackVariant "silverwood_thaumium_scepter" 解析即可得 15000
* (银木杆 10000 × 权杖 1.5),无需依赖栈上的部件数据。
*/
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);
@@ -7,13 +7,13 @@ import java.util.Map;
import net.minecraft.resources.ResourceLocation;
import appeng.api.config.Actionable;
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;
@@ -101,11 +101,28 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider
@Override
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
if (amount <= 0) return 0;
// 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化)
var nativeInjected = injectNative(aspectId, amount, simulate);
if (nativeInjected < amount) {
// 原生存储空间不足时剩余部分退回内存 monitor(供注魔供应器等内部读取)
long remaining = amount - nativeInjected;
if (!simulate) {
long current = monitor.get(aspectId);
monitor.set(aspectId, current + amount);
monitor.set(aspectId, current + remaining);
}
return 0;
return amount - nativeInjected - remaining; // 全部接受(monitor 兜底)
}
return 0; // 全部写入原生存储
}
private long injectNative(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.poweredInsert(
grid.getEnergyService(), storage, key, amount,
IActionSource.empty(), simulate ? Actionable.SIMULATE : Actionable.MODULATE);
}
@Override
@@ -34,11 +34,11 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
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",
tooltip.add(Component.translatable("thaumicenergistics.jade.current_aspect",
Component.translatable(aspectKey)));
}
@@ -93,8 +93,8 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
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());
if (chamber.getEssentiaType(null) != null) {
data.putString(TAG_ASPECT, chamber.getEssentiaType(null).getTag());
}
}
}
@@ -11,7 +11,6 @@ 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
@@ -25,7 +24,7 @@ public class WirelessEssentiaTerminalItem extends WirelessTerminalItem {
@Override
public MenuType<?> getMenuType() {
return MEStorageMenu.WIRELESS_TYPE;
return thaumicenergistics.init.ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get();
}
@Override
@@ -0,0 +1,80 @@
package thaumicenergistics.common.network;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
/**
* 客户端→服务端:源质容器存入 ME 网络。
* <ul>
* <li>手持(carried/主手)源质容器右键 → 源质存入,空容器放回玩家背包(slotIndex=-1)。</li>
* <li>Shift+右键玩家背包格 → 源质存入,空容器留在原背包格(slotIndex>=0)。</li>
* </ul>
*/
public record EssentiaTerminalDepositC2SPacket(int containerId, int slotIndex,
net.minecraft.world.item.ItemStack sourceStack) implements CustomPacketPayload {
public static final Type<EssentiaTerminalDepositC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("essentia_terminal_deposit"));
public static final StreamCodec<RegistryFriendlyByteBuf, EssentiaTerminalDepositC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, EssentiaTerminalDepositC2SPacket::containerId,
ByteBufCodecs.VAR_INT, EssentiaTerminalDepositC2SPacket::slotIndex,
net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC, EssentiaTerminalDepositC2SPacket::sourceStack,
EssentiaTerminalDepositC2SPacket::new
);
/** 手持源质物品存入(客户端可能持有在 carried 或主手)。 */
public static EssentiaTerminalDepositC2SPacket fromClientStack(int containerId, net.minecraft.world.item.ItemStack stack) {
return new EssentiaTerminalDepositC2SPacket(containerId, -1, stack.copy());
}
/** 从玩家背包格存入(Shift+右键背包格)。 */
public static EssentiaTerminalDepositC2SPacket fromSlot(int containerId, int slotIndex, net.minecraft.world.item.ItemStack stack) {
return new EssentiaTerminalDepositC2SPacket(containerId, slotIndex, stack.copy());
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
// 任意 ME 终端菜单都支持:源质容器右键 → 源质存入网络
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof appeng.menu.me.common.MEStorageMenu menu
&& menu instanceof thaumicenergistics.common.container.ThEEssentiaMenuAccess acc) {
var player = menu.getPlayerInventory().player;
// 源质物品:优先包携带的 sourceStack(客户端真实来源);否则回退菜单 carried/主手
var source = !this.sourceStack.isEmpty() ? this.sourceStack
: (!menu.getCarried().isEmpty() ? menu.getCarried()
: player.getMainHandItem());
if (!source.isEmpty()) {
var result = thaumicenergistics.common.container.EssentiaFillHelper.extractContainerContent(
acc.thE_getStorage(), acc.thE_getEnergySource(),
menu.getActionSource(), source);
if (result != null) {
if (this.slotIndex >= 0) {
// Shift+右键背包格:空容器留在原背包格
player.getInventory().setItem(this.slotIndex, result);
} else {
// 手持:清空 carried,空容器放回玩家背包(不留 carried,防被 AE2 自动放入网络)
menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
if (!result.isEmpty()) {
if (!player.getInventory().add(result)) {
player.drop(result, false);
}
}
}
}
}
menu.broadcastChanges();
}
});
}
}
@@ -0,0 +1,60 @@
package thaumicenergistics.common.network;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
/**
* 客户端→服务端:源质终端右键源质条目装罐。
* 客户端已把 serial 解析为 aspect id(服务端无 client repo,无法解析 serial)。
* 携带 sourceStack:罐子可能持于主手(未进入 carried),服务端直接用客户端栈,
* 避免只读 menu.getCarried() 为空导致装罐静默失败。
*/
public record EssentiaTerminalFillC2SPacket(int containerId, ResourceLocation aspectId,
ItemStack sourceStack) implements CustomPacketPayload {
public static final Type<EssentiaTerminalFillC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("essentia_terminal_fill"));
public static final StreamCodec<RegistryFriendlyByteBuf, EssentiaTerminalFillC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, EssentiaTerminalFillC2SPacket::containerId,
ResourceLocation.STREAM_CODEC, EssentiaTerminalFillC2SPacket::aspectId,
ItemStack.OPTIONAL_STREAM_CODEC, EssentiaTerminalFillC2SPacket::sourceStack,
EssentiaTerminalFillC2SPacket::new
);
/** 装罐:sourceStack 为客户端解析出的源质容器(优先 carried,为空回退主手)。 */
public static EssentiaTerminalFillC2SPacket create(int containerId, ResourceLocation aspectId, ItemStack stack) {
return new EssentiaTerminalFillC2SPacket(containerId, aspectId, stack.copy());
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
// 通用化:任意 ME 终端菜单都支持右键源质条目装罐(源质终端/奥术合成终端/AE2 ME 终端等)
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof appeng.menu.me.common.MEStorageMenu menu
&& menu instanceof thaumicenergistics.common.container.ThEEssentiaMenuAccess acc) {
var player = menu.getPlayerInventory().player;
// 源质物品:优先包携带的 sourceStack(客户端真实来源);否则回退菜单 carried/主手
var source = !this.sourceStack.isEmpty() ? this.sourceStack
: (!menu.getCarried().isEmpty() ? menu.getCarried()
: player.getMainHandItem());
thaumicenergistics.common.container.EssentiaFillHelper.fillContainerFromGrid(
acc.thE_getStorage(), acc.thE_getEnergySource(),
menu.getActionSource(), player, source, this.aspectId);
menu.broadcastChanges();
}
});
}
}
@@ -50,6 +50,20 @@ public final class ThENetworking {
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
// EssentiaTerminalFillC2SPacket — C2S(客户端→服务端,源质终端右键装罐)
registrar.playToServer(
EssentiaTerminalFillC2SPacket.TYPE,
EssentiaTerminalFillC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// EssentiaTerminalDepositC2SPacket — C2S(客户端→服务端,手持源质容器右键存入网络)
registrar.playToServer(
EssentiaTerminalDepositC2SPacket.TYPE,
EssentiaTerminalDepositC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// ResearchGateErrorPacket — S2C(服务端→客户端,研究未解锁拦截提示,携带研究键)
registrar.playToClient(
ResearchGateErrorPacket.TYPE,
@@ -60,7 +60,6 @@ public class EssentiaExportBusPart extends IOBusPart
private static final double IDLE_POWER = 0.5;
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA;
private static final int TRANSFER_RATE = 8;
private boolean isWorkingFlag = false;
@@ -70,6 +69,14 @@ public class EssentiaExportBusPart extends IOBusPart
private long storedEnergy = 0;
/**
* 单次导出量 = 基础速率 8 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。
*/
private long getExportRate() {
return 8L * Math.max(1, getOperationsPerTick());
}
public EssentiaExportBusPart(IPartItem<?> partItem) {
super(TickRates.ExportBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
this.getMainNode().setIdlePowerUsage(IDLE_POWER);
@@ -122,7 +129,7 @@ public class EssentiaExportBusPart extends IOBusPart
if (key == null) return false;
long extracted = grid.getStorageService().getInventory().extract(
key, TRANSFER_RATE, Actionable.MODULATE, IActionSource.ofMachine(this));
key, getExportRate(), Actionable.MODULATE, IActionSource.ofMachine(this));
if (extracted <= 0) return false;
// 尝试注入到相邻方块:先 IAspectContainer,再回退到 IEssentiaTransport
@@ -62,6 +62,14 @@ public class EssentiaImportBusPart extends IOBusPart
private boolean isWorkingFlag = false;
/**
* 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。
* AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。
*/
private int getTransferRate() {
return TRANSFER_RATE * Math.max(1, getOperationsPerTick());
}
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
selection -> { },
keyType -> keyType == AEssentiaKeyType.INSTANCE);
@@ -104,7 +112,7 @@ public class EssentiaImportBusPart extends IOBusPart
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE);
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, getTransferRate());
if (extracted == null || extracted.isEmpty()) return false;
long injected = this.injectToNetwork(grid, extracted);
@@ -10,7 +10,6 @@ import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel;
import appeng.api.util.KeyTypeSelection;
import appeng.items.parts.PartModels;
import appeng.menu.me.common.MEStorageMenu;
import appeng.parts.PartModel;
import appeng.parts.reporting.AbstractTerminalPart;
import thaumicenergistics.ThaumicEnergistics;
@@ -54,7 +53,7 @@ public class EssentiaTerminalPart extends AbstractTerminalPart {
@Override
public MenuType<?> getMenuType(Player player) {
return MEStorageMenu.TYPE;
return thaumicenergistics.init.ModMenuTypes.ESSENTIA_TERMINAL.get();
}
@Override
@@ -53,6 +53,9 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
/** 抽取颜色在没有新抽取后保持点亮的时间(ms),超时清除。 */
private static final long TIME_TO_CLEAR = 500L;
/** 懒 tick 周期:固定 30 tick(vis 传输为低频操作,无需实时轮询)。 */
private static final int TICK_RATE = 30;
private static final String NBT_KEY_UID = "uid";
private static final String NBT_KEY_IS_PROVIDER = "isProvider";
@@ -290,7 +293,7 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
@Override
public TickingRequest getTickingRequest(IGridNode node) {
return new TickingRequest(30, 30, false);
return new TickingRequest(TICK_RATE, TICK_RATE, false);
}
@Override
@@ -51,7 +51,6 @@ import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.crafting.IArcaneRecipe;
import thaumicenergistics.api.grid.IEssentiaGrid;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
@@ -314,7 +313,6 @@ public class TileArcaneAssembler extends ThETileBase
boolean enoughVis = hasEnoughVisForCraft();
if (!enoughVis) {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: vis 不足,等待补充 (storedVis cvis={})", storedVis.visSize());
return TickRateModulation.SAME;
}
@@ -331,7 +329,6 @@ public class TileArcaneAssembler extends ThETileBase
long inserted = storage.getInventory().insert(
outputKey, output.getCount(), Actionable.SIMULATE, actionSource);
if (inserted < output.getCount()) {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 产物无法插入网络(模拟 inserted={}/{}),等待", inserted, output.getCount());
return TickRateModulation.SAME;
}
@@ -339,7 +336,6 @@ public class TileArcaneAssembler extends ThETileBase
storage.getInventory().insert(
outputKey, output.getCount(), Actionable.MODULATE, actionSource);
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 完成!插入产物 {}x{} 到网络", outputKey, output.getCount());
isCrafting = false;
currentRecipe = null;
@@ -359,11 +355,8 @@ public class TileArcaneAssembler extends ThETileBase
double extracted = energy.extractAEPower(need, Actionable.MODULATE, PowerMultiplier.CONFIG);
if (extracted >= need * 0.9) {
craftTickCounter += ticksSinceLast;
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need);
// 节流同步合成进度到客户端(GUI 第7格进度条动画)
maybeMarkVisDirty();
} else {
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need);
}
}
@@ -446,19 +439,13 @@ public class TileArcaneAssembler extends ThETileBase
ArcaneAssemblerPattern ap = buildArcanePattern(acp);
if (ap != null) {
result.add(ap);
ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: pattern primaryOutput={}, definition={}",
ap.getPrimaryOutput(), ap.getDefinition());
}
}
ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: kCore valid={}, handler patterns={}, returned={}",
!kCore.isEmpty(), patterns.size(), result.size());
return result;
}
@Override
public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) {
ThaumicEnergistics.LOG.info("[AA] pushPattern(provider) called, pattern={}, isArcanePattern={}",
patternDetails, patternDetails instanceof ArcaneAssemblerPattern);
if (patternDetails instanceof ArcaneAssemblerPattern arcanePattern) {
return beginCrafting(arcanePattern);
}
@@ -490,9 +477,6 @@ public class TileArcaneAssembler extends ThETileBase
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
AspectList aspects = arcanePattern.getAspects();
ItemStack result = arcanePattern.getResultStack();
ThaumicEnergistics.LOG.info("[AA] beginCrafting: isCrafting={}, active={}, aspectsEmpty={}, resultEmpty={}",
isCrafting, mainNode.isActive(),
aspects == null || aspects.isEmpty(), result.isEmpty());
if (isCrafting || !mainNode.isActive()) return false;
if (aspects == null || aspects.isEmpty() || result.isEmpty()) return false;
@@ -504,7 +488,6 @@ public class TileArcaneAssembler extends ThETileBase
this.internalInventory.setItem(TARGET_SLOT, result.copy());
setChanged();
markForUpdate();
ThaumicEnergistics.LOG.info("[AA] beginCrafting: STARTED, result={}, aspects={}", result, aspects.visSize());
return true;
}
@@ -593,7 +576,6 @@ public class TileArcaneAssembler extends ThETileBase
// 守卫 getGrid()!=null:装配器未连 ME 网络(无相邻 cable)时 getGrid() 为 null,
// 此时 requestUpdate 内部 node.getGrid().getCraftingService() 会 NPE,导致 kCore 插入事务回滚 → "无法绑定"。
if (level != null && !level.isClientSide() && mainNode.getGrid() != null) {
ThaumicEnergistics.LOG.info("[AA] kCore slot changed → requestUpdate");
ICraftingProvider.requestUpdate(mainNode);
}
}
@@ -40,12 +40,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private static final int MAX_ESSENTIA = 64;
private static final int BASE_BURN_TICKS = 800;
private static final double BASE_AE_PER_TICK = 20.0;
private static final double BASE_AE_PER_TICK = 200.0;
private static final int IDLE_POWER = 1;
// 能量槽配置:8KFE = 8000 AE,对外最大输出 400 AE/t
public static final double MAX_ENERGY_STORAGE = 8000.0;
public static final double MAX_OUTPUT_PER_TICK = 400.0;
// 能量槽配置:16KFE = 16000 AE,对外最大输出 1000 AE/t
public static final double MAX_ENERGY_STORAGE = 16000.0;
public static final double MAX_OUTPUT_PER_TICK = 2000.0;
// 懒 tick 周期:燃烧中 10 tick、空闲 40 tick(能量生产无需实时响应,低频即可)
private static final int TICK_RATE_BURNING = 10;
private static final int TICK_RATE_IDLE = 40;
protected final IManagedGridNode mainNode;
private final IGridNodeListener<TileEssentiaVibrationChamber> nodeListener = new IGridNodeListener<>() {
@@ -62,7 +66,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private double aePerTick;
private int storedEssentia;
private Aspect currentAspect;
private Aspect suctionAspect = Aspect.ENERGY;
private double storedEnergy;
@@ -97,7 +100,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override
public TickingRequest getTickingRequest(IGridNode node) {
return new TickingRequest(10, 40, false);
return new TickingRequest(TICK_RATE_BURNING, TICK_RATE_IDLE, false);
}
@Override
@@ -177,8 +180,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
int srcAmt = source.getEssentiaAmount(sourceSide);
if (!canOut) continue;
if (srcType == null) continue;
String srcTag = srcType.getTag();
if (!srcTag.equals("ignis") && !srcTag.equals("potentia") && !srcTag.equals("energy")) continue;
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
if (space <= 0) return;
@@ -187,7 +189,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
if (taken > 0) {
storedEssentia += taken;
currentAspect = srcType;
suctionAspect = srcType;
// suctionAspect 固定为 null(通配吸力),不锁定单一类型
setChanged();
return;
}
@@ -240,9 +242,25 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
private boolean startBurning() {
if (currentAspect == null) currentAspect = Aspect.FIRE;
boolean isPotentia = currentAspect != null && (currentAspect == Aspect.ENERGY || currentAspect.getTag().equals("potentia") || currentAspect.getTag().equals("energy"));
int burnTime = isPotentia ? (int)(BASE_BURN_TICKS / 1.6f) : BASE_BURN_TICKS / 2;
double power = isPotentia ? BASE_AE_PER_TICK * 1.6 : BASE_AE_PER_TICK;
String tag = currentAspect.getTag();
boolean isPotentia = currentAspect == Aspect.ENERGY || tag.equals("potentia") || tag.equals("energy");
boolean isOther = !tag.equals("ignis") && !isPotentia;
int burnTime;
double power;
if (isPotentia) {
// 能量:1.6 倍功率,燃烧更久
burnTime = (int)(BASE_BURN_TICKS / 1.6f);
power = BASE_AE_PER_TICK * 1.6;
} else if (isOther) {
// 其他所有要素:基础效率的 1/2
burnTime = BASE_BURN_TICKS / 2;
power = BASE_AE_PER_TICK / 2.0;
} else {
// 火:基础效率
burnTime = BASE_BURN_TICKS / 2;
power = BASE_AE_PER_TICK;
}
burnTicksRemaining = burnTime;
totalBurnTicks = burnTime;
@@ -254,17 +272,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override public boolean canInputFrom(Direction side) { return true; }
@Override public boolean canOutputTo(Direction side) { return true; }
@Override public void setSuction(Aspect aspect, int amount) {
this.suctionAspect = aspect;
// 通配吸力:始终吸所有源质类型,忽略外部设置
}
@Override public Aspect getSuctionType(Direction side) {
return suctionAspect;
return null; // null = 通配吸力:管道可输送任意类型源质
}
@Override public int getSuctionAmount(Direction side) { return 128; }
private boolean isFuelAspect(Aspect aspect) {
if (aspect == null) return false;
String tag = aspect.getTag();
return tag.equals("ignis") || tag.equals("potentia") || tag.equals("energy");
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
return aspect != null;
}
@Override
@@ -298,11 +315,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
@Override public boolean renderExtendedTube() { return false; }
public void setSuctionType(Aspect aspect) {
if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) {
suctionAspect = aspect;
setChanged();
markForUpdate();
}
// 通配吸力模式下无需设置类型;保留方法避免编译错误
}
public int getStoredEssentia() { return storedEssentia; }
@@ -338,7 +351,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
storedEnergy = tag.getDouble("StoredEnergy");
if (storedEnergy > MAX_ENERGY_STORAGE) storedEnergy = MAX_ENERGY_STORAGE;
if (tag.contains("AspectTag")) currentAspect = Aspect.getAspect(tag.getString("AspectTag"));
if (tag.contains("SuctionTag")) suctionAspect = Aspect.getAspect(tag.getString("SuctionTag"));
// suctionAspect 固定为 null(通配吸力),不读取 SuctionTag
}
@Override
@@ -352,6 +365,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
tag.putInt("StoredEssentia", storedEssentia);
tag.putDouble("StoredEnergy", storedEnergy);
if (currentAspect != null) tag.putString("AspectTag", currentAspect.getTag());
if (suctionAspect != null) tag.putString("SuctionTag", suctionAspect.getTag());
// suctionAspect 固定为 null(通配吸力),不写入 SuctionTag
}
}
@@ -1,5 +1,6 @@
package thaumicenergistics.init;
import appeng.api.implementations.menuobjects.IPortableTerminal;
import appeng.api.storage.ITerminalHost;
import appeng.menu.implementations.MenuTypeBuilder;
import net.minecraft.core.registries.Registries;
@@ -27,8 +28,16 @@ public final class ModMenuTypes {
REGISTRY.register("essentia_vibration_chamber", () -> IMenuTypeExtension.create(ContainerEssentiaVibrationChamber::new));
public static final Supplier<MenuType<ContainerArcaneAssembler>> ARCANE_ASSEMBLER =
REGISTRY.register("arcane_assembler", () -> IMenuTypeExtension.create(ContainerArcaneAssembler::new));
public static final Supplier<MenuType<ContainerEssentiaCellTerminal>> ESSENTIA_TERMINAL =
REGISTRY.register("essentia_terminal", () -> IMenuTypeExtension.create(ContainerEssentiaCellTerminal::new));
/** 源质终端菜单:有线段(挂在线缆上的终端部件)。 */
public static final Supplier<MenuType<EssentiaTerminalMenu>> ESSENTIA_TERMINAL =
REGISTRY.register("essentia_terminal", () ->
MenuTypeBuilder.create(EssentiaTerminalMenu::new, ITerminalHost.class)
.buildUnregistered(ThaumicEnergistics.id("essentia_terminal")));
/** 源质终端菜单:无线段(手持无线源质终端)。 */
public static final Supplier<MenuType<EssentiaTerminalMenu>> WIRELESS_ESSENTIA_TERMINAL =
REGISTRY.register("wireless_essentia_terminal", () ->
MenuTypeBuilder.create(EssentiaTerminalMenu::new, IPortableTerminal.class)
.buildUnregistered(ThaumicEnergistics.id("wireless_essentia_terminal")));
public static final Supplier<MenuType<ContainerKnowledgeInscriber>> KNOWLEDGE_INSCRIBER =
REGISTRY.register("knowledge_inscriber", () -> IMenuTypeExtension.create(ContainerKnowledgeInscriber::new));
public static final Supplier<MenuType<ContainerDistillationPatternEncoder>> DISTILLATION_ENCODER =
@@ -38,28 +47,28 @@ public final class ModMenuTypes {
REGISTRY.register("essentia_import_bus", () ->
MenuTypeBuilder.create(ContainerEssentiaImportBus::new, EssentiaImportBusPart.class)
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
.build("essentia_import_bus"));
.buildUnregistered(ThaumicEnergistics.id("essentia_import_bus")));
public static final Supplier<MenuType<ContainerEssentiaExportBus>> ESSENTIA_EXPORT_BUS =
REGISTRY.register("essentia_export_bus", () ->
MenuTypeBuilder.create(ContainerEssentiaExportBus::new, EssentiaExportBusPart.class)
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
.build("essentia_export_bus"));
.buildUnregistered(ThaumicEnergistics.id("essentia_export_bus")));
public static final Supplier<MenuType<ContainerEssentiaStorageBus>> ESSENTIA_STORAGE_BUS =
REGISTRY.register("essentia_storage_bus", () ->
MenuTypeBuilder.create(ContainerEssentiaStorageBus::new, EssentiaStorageBusPart.class)
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
.build("essentia_storage_bus"));
.buildUnregistered(ThaumicEnergistics.id("essentia_storage_bus")));
public static final Supplier<MenuType<ContainerArcaneCraftingTerminal>> ARCANE_CRAFTING_TERMINAL =
REGISTRY.register("arcane_crafting_terminal", () ->
MenuTypeBuilder.create(ContainerArcaneCraftingTerminal::new, ITerminalHost.class)
.build("arcane_crafting_terminal"));
.buildUnregistered(ThaumicEnergistics.id("arcane_crafting_terminal")));
public static final Supplier<MenuType<ContainerEssentiaLevelEmitter>> ESSENTIA_LEVEL_EMITTER =
REGISTRY.register("essentia_level_emitter", () ->
MenuTypeBuilder.create(ContainerEssentiaLevelEmitter::new, EssentiaLevelEmitterPart.class)
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
.build("essentia_level_emitter"));
.buildUnregistered(ThaumicEnergistics.id("essentia_level_emitter")));
}
@@ -49,9 +49,8 @@ public class ThEJeiPlugin implements IModPlugin {
Class.forName("tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverters");
Class.forName("thaumicenergistics.integration.jei.Ae2JeiCompat")
.getMethod("register").invoke(null);
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration detected, essentia drag-conversion registered");
} catch (ClassNotFoundException e) {
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration not present, essentia drag-conversion unavailable (optional)");
// AE2-JEI-Integration 未安装:跳过源质拖拽转换(可选功能,不影响其他 JEI 功能)
} catch (ReflectiveOperationException e) {
ThaumicEnergistics.LOG.warn("[JEI] AE2-JEI-Integration registration failed: {}", e.toString());
}
@@ -83,7 +82,6 @@ public class ThEJeiPlugin implements IModPlugin {
@Override
public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) {
ThaumicEnergistics.LOG.info("[JEI] Registering ThEUseCraftingRecipeTransfer for ArcaneCraftingTerminal");
// 注册 IRecipeTransferInfo 让 JEI 显示 "+" 按钮
registration.addRecipeTransferHandler(new ArcaneCraftingTransferInfo());
registration.addRecipeTransferHandler(new ArcaneRecipeTransferInfo());
@@ -1,5 +1,7 @@
package thaumicenergistics.integration.jei;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
@@ -24,10 +26,14 @@ import net.neoforged.neoforge.network.PacketDistributor;
import thaumcraft.integration.jei.JeiArcaneRecipe;
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
import thaumicenergistics.init.ModMenuTypes;
import static appeng.integration.modules.itemlists.TransferHelper.BLUE_PLUS_BUTTON_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.BLUE_SLOT_HIGHLIGHT_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.ORANGE_PLUS_BUTTON_COLOR;
import static appeng.integration.modules.itemlists.TransferHelper.RED_SLOT_HIGHLIGHT_COLOR;
/**
* 奥术合成终端专用的 JEI 奥术配方转移处理器。
* 点击 "+" 时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。
@@ -65,13 +71,31 @@ public class ThEUseArcaneRecipeTransfer
boolean maxTransfer,
boolean doTransfer) {
ThaumicEnergistics.LOG.info("[JEI-Arcane] transferRecipe called, recipeId={}, doTransfer={}",
recipe.id(), doTransfer);
boolean craftMissing = AbstractContainerScreen.hasControlDown();
// 预览:始终 null(绿色按钮),由服务端处理实际缺失
// 构建 3x3 合成格 slot→ingredient 映射,检测材料缺失(与普通配方一致)
var slotToIngredientMap = buildSlotToIngredientMap(recipe);
var missingSlots = menu.findMissingIngredients(slotToIngredientMap);
// 全部缺失 → 红色,无法点击
if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) {
var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT);
var missingSlotViews = missingSlots.missingSlots().stream()
.map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null)
.filter(Objects::nonNull)
.toList();
return helper.createUserErrorForMissingSlots(
appeng.core.localization.ItemModText.NO_ITEMS.text(), missingSlotViews);
}
// 预览:检查缺失情况,返回对应的错误渲染
if (!doTransfer) {
if (missingSlots.totalSize() != 0) {
int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR;
// 有缺料 → Type.ERROR 阻止转移(按钮不可点),有料也不允许部分填充
return new ThEUseCraftingRecipeTransfer.MissingSlotsError(
missingSlots, craftMissing, color, missingSlots.anyMissing(), menu);
}
return null;
}
@@ -88,10 +112,23 @@ public class ThEUseArcaneRecipeTransfer
// 奥术配方不在 vanilla 配方管理器中,recipeId 传 null
// 服务端会用 Ingredient.of(templateItems) 重建,然后从 ME 网络提取
var packet = new FillCraftingGridFromRecipePacket(null, templateItems, craftMissing);
ThaumicEnergistics.LOG.info("[JEI-Arcane] Sending FillCraftingGridFromRecipePacket, templateCount={}",
templateItems.stream().filter(s -> !s.isEmpty()).count());
PacketDistributor.sendToServer(packet);
return null;
}
/**
* 根据奥术配方 inputs(9 槽 List<List<ItemStack>>)构建 3x3 合成格 slot → ingredient 映射。
*/
private static Map<Integer, Ingredient> buildSlotToIngredientMap(JeiArcaneRecipe recipe) {
Map<Integer, Ingredient> map = new HashMap<>();
var inputs = recipe.inputs();
for (int i = 0; i < inputs.size() && i < 9; i++) {
var variants = inputs.get(i);
if (variants != null && !variants.isEmpty()) {
map.put(i, Ingredient.of(variants.stream()));
}
}
return map;
}
}
@@ -82,8 +82,6 @@ public class ThEUseCraftingRecipeTransfer
boolean doTransfer) {
var recipe = recipeHolder.value();
ThaumicEnergistics.LOG.info("[JEI] transferRecipe called, recipeId={}, doTransfer={}, player={}",
recipeHolder.id(), doTransfer, player.getName().getString());
if (recipe.getType() != RecipeType.CRAFTING) {
ThaumicEnergistics.LOG.warn("[JEI] Not a crafting recipe, type={}", recipe.getType());
return helper.createInternalError();
@@ -96,16 +94,14 @@ public class ThEUseCraftingRecipeTransfer
}
boolean craftMissing = AbstractContainerScreen.hasControlDown();
// 自行构建 slot→ingredient 映射(0-8 对应 3x3 合成格),
// 不依赖 helper.getGuiSlotIndexToIngredientMap(需要 IRecipeTransferInfo 才能正确工作)
var slotToIngredientMap = buildSlotToIngredientMap(recipe);
// 用 JEI 官方映射 helper.getGuiSlotIndexToIngredientMap:配方居中摆入 3x3 网格,
// 键空间与 JEI 输入槽视图完全一致。之前用 ensure3by3CraftingMatrix(左上对齐填充),
// 对非 3x3 配方(如 2x2 笔与墨)索引错位,导致缺失高亮画到错误的格子上。
var slotToIngredientMap = helper.getGuiSlotIndexToIngredientMap(recipeHolder);
var missingSlots = menu.findMissingIngredients(slotToIngredientMap);
ThaumicEnergistics.LOG.info("[JEI] missingSlots: missing={}, craftable={}, total={}",
missingSlots.missingSlots().size(), missingSlots.craftableSlots().size(), missingSlots.totalSize());
// 全部缺失 → 红色,无法点击
if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) {
ThaumicEnergistics.LOG.info("[JEI] All items missing, returning NO_ITEMS error");
var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT);
var missingSlotViews = missingSlots.missingSlots().stream()
.map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null)
@@ -119,7 +115,8 @@ public class ThEUseCraftingRecipeTransfer
if (!doTransfer) {
if (missingSlots.totalSize() != 0) {
int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR;
return new MissingSlotsError(missingSlots, craftMissing, color);
// 有缺料 → USER_FACING 阻止转移(按钮不可点);仅可合成(craftable)不阻止(可触发自动合成)
return new MissingSlotsError(missingSlots, craftMissing, color, missingSlots.anyMissing(), menu);
}
// 全部物品都有 → null = 绿色按钮
return null;
@@ -145,42 +142,28 @@ public class ThEUseCraftingRecipeTransfer
}
var packet = new FillCraftingGridFromRecipePacket(recipeId, templateItems, craftMissing);
ThaumicEnergistics.LOG.info("[JEI] Sending FillCraftingGridFromRecipePacket, recipeId={}, templateCount={}, craftMissing={}",
recipeId, templateItems.stream().filter(s -> !s.isEmpty()).count(), craftMissing);
PacketDistributor.sendToServer(packet);
return null;
}
/**
* 根据配方构建 3x3 合成格的 slot index → ingredient 映射。
* 使用 CraftingRecipeUtil.ensure3by3CraftingMatrix 确保 9 格对齐,
* 与实际转移时的索引体系完全一致。
*/
private static Map<Integer, Ingredient> buildSlotToIngredientMap(CraftingRecipe recipe) {
var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe);
Map<Integer, Ingredient> map = new HashMap<>();
for (int i = 0; i < ingredients.size() && i < 9; i++) {
var ing = ingredients.get(i);
if (!ing.isEmpty()) {
map.put(i, ing);
}
}
return map;
}
/**
* 缺失/可合成物品的预览错误渲染器。
* 参考 AE2-JEI-Integration.UseCraftingRecipeTransfer.ErrorRenderer。
* 供奥术配方转移(ThEUseArcaneRecipeTransfer)复用。
* blockTransfer=true 时返回 ERROR(阻止转移,按钮灰/橙不可点)。
*/
private record MissingSlotsError(
public record MissingSlotsError(
ContainerArcaneCraftingTerminal.MissingIngredientSlots indices,
boolean craftMissing,
int color) implements IRecipeTransferError {
int color,
boolean blockTransfer,
ContainerArcaneCraftingTerminal menu) implements IRecipeTransferError {
@Override
public Type getType() {
return Type.COSMETIC;
// USER_FACING 阻止转移(allowsTransfer=false)且显示错误;COSMETIC 仅预览
return blockTransfer ? Type.USER_FACING : Type.COSMETIC;
}
@Override
@@ -195,16 +178,20 @@ public class ThEUseCraftingRecipeTransfer
poseStack.pushPose();
poseStack.translate(recipeX, recipeY, 0);
var slotViews = slots.getSlotViews(RecipeIngredientRole.INPUT);
for (int i = 0; i < slotViews.size(); i++) {
var slotView = slotViews.get(i);
// 用 JEI 槽视图 drawHighlight 画高亮(AE2-JEI-Integration 官方做法)。
// 不能用 menu.slots 的容器坐标:奥术终端槽位顺序与配方格不对应,
// 且 translate(recipeX,recipeY) 后再用相对坐标会双重偏移 → 红框画到屏幕外。
var inputSlots = slots.getSlotViews(RecipeIngredientRole.INPUT);
for (int i = 0; i < 9; i++) {
boolean missing = indices.missingSlots().contains(i);
boolean craftable = indices.craftableSlots().contains(i);
if (missing || craftable) {
slotView.drawHighlight(
guiGraphics,
if (!missing && !craftable) continue;
if (i >= inputSlots.size()) continue;
var slotView = inputSlots.get(i);
slotView.drawHighlight(guiGraphics,
missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR);
}
ThaumicEnergistics.LOG.info("[JEI-Transfer] showError slot={} missing={} craftable={}",
i, missing, craftable);
}
poseStack.popPose();
@@ -221,15 +208,15 @@ public class ThEUseCraftingRecipeTransfer
int craftable = indices.craftableSlots().size();
if (missing > 0) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.missing", missing));
"thaumicenergistics.jei.transfer.missing", missing));
}
if (craftable > 0) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.craftable", craftable));
"thaumicenergistics.jei.transfer.craftable", craftable));
}
if (craftMissing) {
tooltip.add(net.minecraft.network.chat.Component.translatable(
"jei.tooltip.transfer.ctrl.click"));
"thaumicenergistics.jei.transfer.ctrl_click"));
}
}
}
@@ -0,0 +1,38 @@
package thaumicenergistics.mixins.ae2;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.storage.MEStorage;
import appeng.menu.me.common.MEStorageMenu;
import thaumicenergistics.common.container.ThEEssentiaMenuAccess;
/**
* 为 {@link MEStorageMenu} 实现 {@link ThEEssentiaMenuAccess} 接口,
* 暴露其 protected 的 storage / energySource 供 ThaumicEnergistics 的
* {@code EssentiaFillHelper} 访问。
* <p>
* 通过 mixin 向目标类追加方法(mixin 内普通方法会被注入到目标类),
* 方法名前缀 thE_ 避免与 AE2 未来版本可能新增的方法冲突。
*/
@Mixin(targets = "appeng.menu.me.common.MEStorageMenu", remap = false)
public abstract class MixinMEStorageMenuAccessors implements ThEEssentiaMenuAccess {
@Shadow(remap = false)
protected MEStorage storage;
@Shadow(remap = false)
protected IEnergySource energySource;
@Override
public MEStorage thE_getStorage() {
return this.storage;
}
@Override
public IEnergySource thE_getEnergySource() {
return this.energySource;
}
}
@@ -0,0 +1,138 @@
package thaumicenergistics.mixins.ae2;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.me.common.RepoSlot;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket;
import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket;
/**
* 让所有 AE2 ME 终端 GUI(ME 终端、合成终端、无线终端等)都支持源质右键交互:
* <ul>
* <li>Shift+右键玩家背包格:格内是源质容器 → 源质存入网络(空容器留原格)。</li>
* <li>右键玩家物品栏/背包槽 → 放行(走 AE2 原版物品交互)。</li>
* <li>右键网络区域(源质条目或空白):手持装有源质的罐子/安瓿 → 源质存入 ME 网络(空容器回背包)。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出源质装罐/装安瓿。</li>
* <li>左键仍是正常物品操作(不受影响)。</li>
* </ul>
* 源质终端({@code GuiEssentiaTerminal})已覆写 {@code mouseClicked},子类覆写优先于 mixin;
* 此 mixin 覆盖其余未覆写的 ME 终端 GUI。
*/
@Mixin(targets = "appeng.client.gui.me.common.MEStorageScreen", remap = false)
public abstract class MixinMEStorageScreen {
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, remap = false)
private void thE_onMouseClickedEssentia(double xCoord, double yCoord, int btn,
CallbackInfoReturnable<Boolean> cir) {
if (btn != 1) {
return;
}
MEStorageScreen<?> self = (MEStorageScreen<?>) (Object) this;
Slot hovered = self.getSlotUnderMouse();
boolean shiftDown = net.minecraft.client.gui.screens.inventory.AbstractContainerScreen.hasShiftDown();
var carried = self.getMenu().getCarried();
// 源质物品来源:优先菜单 carried(拾取物品);为空时回退到玩家主手物品
// (GUI 打开时主手物品不自动进入 carried,除非先左键点击拿起)
ItemStack sourceStack = !carried.isEmpty() ? carried
: self.getMenu().getPlayerInventory().player.getMainHandItem();
boolean isContainer = isSourceContainer(sourceStack);
boolean containerHasEssentia = isContainer && hasContainerEssentia(sourceStack);
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && hovered != null && self.getMenu().isPlayerSideSlot(hovered)) {
var slotStack = hovered.getItem();
if (slotStack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
self.getMenu().containerId, hovered.getSlotIndex(), slotStack));
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(AE2 原版 Shift+右键快速移动)
return;
}
// 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互)
if (!shiftDown && hovered != null && self.getMenu().isPlayerSideSlot(hovered)) {
return;
}
// Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当作普通物品放入网络
if (shiftDown && isContainer) {
if (containerHasEssentia) {
// 立即本地清空 carried/主手,防止 AE2 后续把空容器当普通物品放入网络
clearSource(self, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
self.getMenu().containerId, sourceStack));
}
cir.setReturnValue(true);
return;
}
// 普通右键网络区域:手持满容器 → 存入源质
if (containerHasEssentia) {
// 立即本地清空 carried/主手,防止 AE2 后续把空容器当普通物品放入网络
clearSource(self, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
self.getMenu().containerId, sourceStack));
cir.setReturnValue(true);
return;
}
// 手持空容器:右键源质条目 → 取出装罐;右键网络其他 → 拦截(防被放入网络)
if (isContainer) {
if (hovered instanceof RepoSlot repoSlot) {
var entry = repoSlot.getEntry();
if (entry != null && entry.getWhat() instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey key) {
PacketDistributor.sendToServer(EssentiaTerminalFillC2SPacket.create(
self.getMenu().containerId, key.getId(), sourceStack));
cir.setReturnValue(true);
return;
}
}
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(AE2 原版右键)
}
/** 清空 source 所在位置(carried 或主手),防止被 AE2 后续当普通物品处理。 */
private static void clearSource(MEStorageScreen<?> self, ItemStack source, ItemStack carried) {
if (!carried.isEmpty() && carried == source) {
self.getMenu().setCarried(net.minecraft.world.item.ItemStack.EMPTY);
} else {
self.getMenu().getPlayerInventory().player.getInventory().setItem(
self.getMenu().getPlayerInventory().player.getInventory().selected,
net.minecraft.world.item.ItemStack.EMPTY);
}
}
/** 源质容器:实现 IEssentiaContainerItem 的物品,或空玻璃安瓿(glass_phial 不实现该接口但可装罐)。 */
private static boolean isSourceContainer(ItemStack stack) {
if (stack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem) {
return true;
}
return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")));
}
/** 若手持物品是源质容器且非空,返回 true。 */
private static boolean hasContainerEssentia(ItemStack stack) {
if (stack.getItem() instanceof thaumcraft.api.aspects.IEssentiaContainerItem container) {
var aspects = container.getAspects(stack);
if (aspects != null && !aspects.isEmpty()) {
return true;
}
}
return false;
}
}
+11
View File
@@ -0,0 +1,11 @@
modLoader="javafml"
loaderVersion="[4,)"
license="MIT"
[[mods]]
modId="thaumicenergistics"
version="2.0.1"
displayName="ThaumicEnergistics"
description="Bridges Thaumcraft Essentia with Applied Energistics 2 ME networks."
authors="Nividica, Community"
logoFile="assets/thaumicenergistics/meta/logo.png"
@@ -35,7 +35,7 @@ side="BOTH"
[[dependencies.thaumicenergistics]]
modId="thaumcraft"
type="required"
versionRange="[0,)"
versionRange="[0.2.2.95,)"
ordering="AFTER"
side="BOTH"
@@ -32,7 +32,16 @@
},
"position": {
"left": 8,
"bottom": 177
"bottom": 179
}
},
"player_inventory_title": {
"text": {
"translate": "container.inventory"
},
"position": {
"left": 8,
"bottom": 94
}
}
},
@@ -111,5 +111,8 @@
"thaumicenergistics.jade.energy_stored": "Stored: %s / %s",
"thaumicenergistics.jade.energy_output": "Output Limit: %s AE/tick",
"thaumicenergistics.jade.tank_full": "Tank full, generation stopped",
"thaumicenergistics.gui.distillation_encoder.not_scanned": "Not scanned"
"thaumicenergistics.gui.distillation_encoder.not_scanned": "Not scanned",
"thaumicenergistics.jei.transfer.missing": "Missing: %d",
"thaumicenergistics.jei.transfer.craftable": "Craftable: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "Use Ctrl to craft missing items"
}
@@ -121,7 +121,7 @@
"tc.research_name.PSEUDO_SCEPTRE": "工艺权杖",
"tc.research_name.PSEUDO_COREGATHER": "傀儡核心:聚集",
"thaumicenergistics.jade.suction_type": "吸力: %s",
"thaumicenergistics.jade.current_aspect": "燃料: %s",
"thaumicenergistics.jade.essentia_stored": "源质: %d / %d",
"thaumicenergistics.jade.burning": "燃烧 %d%%, %.1f AE/tick",
"thaumicenergistics.aspect.ignis": "火",
@@ -130,5 +130,8 @@
"thaumicenergistics.jade.energy_stored": "已存储: %s / %s",
"thaumicenergistics.jade.energy_output": "输出上限: %s AE/tick",
"thaumicenergistics.jade.tank_full": "能量槽已满,停止发电",
"thaumicenergistics.gui.distillation_encoder.not_scanned": "未扫描"
"thaumicenergistics.gui.distillation_encoder.not_scanned": "未扫描",
"thaumicenergistics.jei.transfer.missing": "缺少: %d",
"thaumicenergistics.jei.transfer.craftable": "可合成: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "按住 Ctrl 合成缺失物品"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

@@ -0,0 +1,12 @@
{
"replace": false,
"values": [
"thaumicenergistics:essentia_provider",
"thaumicenergistics:infusion_provider",
"thaumicenergistics:arcane_assembler",
"thaumicenergistics:essentia_vibration_chamber",
"thaumicenergistics:knowledge_inscriber",
"thaumicenergistics:essentia_cell_workbench",
"thaumicenergistics:distillation_encoder"
]
}
@@ -0,0 +1,12 @@
{
"replace": false,
"values": [
"thaumicenergistics:essentia_provider",
"thaumicenergistics:infusion_provider",
"thaumicenergistics:arcane_assembler",
"thaumicenergistics:essentia_vibration_chamber",
"thaumicenergistics:knowledge_inscriber",
"thaumicenergistics:essentia_cell_workbench",
"thaumicenergistics:distillation_encoder"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

@@ -8,11 +8,13 @@
"ae2.MixinCraftingTreeProcess",
"ae2.MixinAbstractTerminalPart",
"ae2.MixinConfigMenuInventory",
"ae2.MixinMEStorageMenuAccessors",
"tc.MixinAspect",
"tc.MixinVisRelayBlock"
],
"client": [
"ae2.MixinCraftConfirmScreen",
"ae2.MixinMEStorageScreen",
"tc.ResearchPageIndexMixin",
"tc.MixinThaumonomiconScreen"
],