Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e9f1fd4e3 | ||
|
|
835a37a0c7 | ||
|
|
da04b67ef0 | ||
|
|
13b981a916 | ||
|
|
286fc2d2c3 |
@@ -16,3 +16,7 @@ progress.md
|
|||||||
/modules/thaumicenergistics-neo/存储体系方案.md
|
/modules/thaumicenergistics-neo/存储体系方案.md
|
||||||
/modules/thaumicenergistics-neo/计划书-配方溯源.md
|
/modules/thaumicenergistics-neo/计划书-配方溯源.md
|
||||||
/.reasonix/
|
/.reasonix/
|
||||||
|
/.tmp_*/
|
||||||
|
__pycache__/
|
||||||
|
/Issue-改善物品方块扫描要素.md
|
||||||
|
/路径.md
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# Project
|
# Project
|
||||||
mod_version=2.0.1
|
mod_version=2.1.1
|
||||||
mod_group=thaumicenergistics
|
mod_group=thaumicenergistics
|
||||||
|
|
||||||
# Mod
|
# Mod
|
||||||
|
|||||||
@@ -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" "$@"
|
||||||
@@ -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.network.ThENetworking.init(modBus);
|
||||||
|
|
||||||
|
thaumicenergistics.common.blocks.ThEWrenchHandler.register();
|
||||||
|
|
||||||
modBus.addListener(this::registerCapabilities);
|
modBus.addListener(this::registerCapabilities);
|
||||||
|
|
||||||
PartModels.registerModels(
|
PartModels.registerModels(
|
||||||
@@ -147,7 +149,14 @@ public final class ThaumicEnergistics {
|
|||||||
event.register(ModMenuTypes.ESSENTIA_CELL_WORKBENCH.get(), GuiEssentiaCellWorkbench::new);
|
event.register(ModMenuTypes.ESSENTIA_CELL_WORKBENCH.get(), GuiEssentiaCellWorkbench::new);
|
||||||
event.register(ModMenuTypes.EVC.get(), GuiEssentiaVibrationChamber::new);
|
event.register(ModMenuTypes.EVC.get(), GuiEssentiaVibrationChamber::new);
|
||||||
event.register(ModMenuTypes.ARCANE_ASSEMBLER.get(), GuiArcaneAssembler::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.KNOWLEDGE_INSCRIBER.get(), GuiKnowledgeInscriber::new);
|
||||||
event.register(ModMenuTypes.DISTILLATION_ENCODER.get(), GuiDistillationPatternEncoder::new);
|
event.register(ModMenuTypes.DISTILLATION_ENCODER.get(), GuiDistillationPatternEncoder::new);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package thaumicenergistics.common.blocks;
|
package thaumicenergistics.common.blocks;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import net.minecraft.core.BlockPos;
|
import net.minecraft.core.BlockPos;
|
||||||
import net.minecraft.core.Direction;
|
import net.minecraft.core.Direction;
|
||||||
import net.minecraft.world.item.context.BlockPlaceContext;
|
import net.minecraft.world.item.context.BlockPlaceContext;
|
||||||
import net.minecraft.world.InteractionHand;
|
|
||||||
import net.minecraft.world.ItemInteractionResult;
|
import net.minecraft.world.ItemInteractionResult;
|
||||||
|
import net.minecraft.world.InteractionHand;
|
||||||
import net.minecraft.world.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
import net.minecraft.world.level.Level;
|
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.BlockState;
|
||||||
import net.minecraft.world.level.block.state.StateDefinition;
|
import net.minecraft.world.level.block.state.StateDefinition;
|
||||||
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
import net.minecraft.world.level.block.state.properties.DirectionProperty;
|
||||||
|
import net.minecraft.world.level.storage.loot.LootParams;
|
||||||
import net.minecraft.world.phys.BlockHitResult;
|
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.ThETileBase;
|
||||||
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
|
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
|
||||||
|
|
||||||
@@ -43,20 +43,16 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock
|
|||||||
@Override
|
@Override
|
||||||
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
|
public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 谐振仓:通配吸力模式,不再需要安瓿瓶切换燃料类型。
|
||||||
|
* 放行默认交互(若后续添加 GUI 则走 getMenuProvider)。
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
|
protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
|
||||||
if (level.isClientSide()) return ItemInteractionResult.SUCCESS;
|
if (level.isClientSide()) return ItemInteractionResult.SUCCESS;
|
||||||
BlockEntity be = level.getBlockEntity(pos);
|
BlockEntity be = level.getBlockEntity(pos);
|
||||||
if (!(be instanceof TileEssentiaVibrationChamber chamber)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
|
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;
|
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);
|
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;
|
package thaumicenergistics.common.blocks;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import net.minecraft.core.BlockPos;
|
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.Block;
|
||||||
import net.minecraft.world.level.block.EntityBlock;
|
import net.minecraft.world.level.block.EntityBlock;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntity;
|
import net.minecraft.world.level.block.entity.BlockEntity;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
import net.minecraft.world.level.block.entity.BlockEntityTicker;
|
||||||
import net.minecraft.world.level.block.entity.BlockEntityType;
|
import net.minecraft.world.level.block.entity.BlockEntityType;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
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.TileEssentiaProvider;
|
||||||
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
|
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
|
||||||
import thaumicenergistics.common.tiles.TileInfusionProvider;
|
import thaumicenergistics.common.tiles.TileInfusionProvider;
|
||||||
@@ -36,6 +44,24 @@ public abstract class ThEBaseEntityBlock extends Block implements EntityBlock {
|
|||||||
return (BlockEntityTicker<T>) ticker;
|
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
|
@Override
|
||||||
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
|
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) {
|
||||||
if (!state.is(newState.getBlock())) {
|
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.IVisDiscountGear;
|
||||||
import thaumcraft.api.IWarpingGear;
|
import thaumcraft.api.IWarpingGear;
|
||||||
import thaumcraft.api.aspects.Aspect;
|
import thaumcraft.api.aspects.Aspect;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
import thaumicenergistics.common.container.slot.ReadOnlySlot;
|
||||||
import thaumicenergistics.common.tiles.TileArcaneAssembler;
|
import thaumicenergistics.common.tiles.TileArcaneAssembler;
|
||||||
import thaumicenergistics.init.ModMenuTypes;
|
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 r = 0; r < PATTERN_ROWS; r++)
|
||||||
for (int c = 0; c < PATTERN_COLS; c++) {
|
for (int c = 0; c < PATTERN_COLS; c++) {
|
||||||
int invIdx = TileArcaneAssembler.PATTERN_SLOT_START + r * PATTERN_COLS + c;
|
int invIdx = TileArcaneAssembler.PATTERN_SLOT_START + r * PATTERN_COLS + c;
|
||||||
addSlot(new SlotItemHandler(machineHandler, invIdx,
|
addSlot(new ReadOnlySlot(machineHandler, invIdx,
|
||||||
PATTERN_X + c * 18, PATTERN_Y + r * 18) {
|
PATTERN_X + c * 18, PATTERN_Y + r * 18));
|
||||||
@Override public boolean mayPlace(ItemStack s) { return false; }
|
|
||||||
@Override public boolean mayPickup(Player p) { return false; }
|
|
||||||
@Override public int getMaxStackSize() { return 1; }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 5. 4张AE2速度卡 =====
|
// ===== 5. 4张AE2速度卡 =====
|
||||||
@@ -182,11 +178,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 6. 目标产物输出槽(只读展示) =====
|
// ===== 6. 目标产物输出槽(只读展示,AE2 式防整理) =====
|
||||||
addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y) {
|
addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y));
|
||||||
@Override public boolean mayPlace(ItemStack s) { return false; }
|
|
||||||
@Override public boolean mayPickup(Player p) { return false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== 7. 4个护甲折扣槽 =====
|
// ===== 7. 4个护甲折扣槽 =====
|
||||||
for (int i = 0; i < ARMOR_COUNT; i++) {
|
for (int i = 0; i < ARMOR_COUNT; i++) {
|
||||||
@@ -202,9 +195,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
|
|||||||
@Override public boolean mayPlace(ItemStack s) {
|
@Override public boolean mayPlace(ItemStack s) {
|
||||||
if (s.getItem() instanceof IVisDiscountGear) return true;
|
if (s.getItem() instanceof IVisDiscountGear) return true;
|
||||||
if (s.getItem() instanceof IWarpingGear) return true;
|
if (s.getItem() instanceof IWarpingGear) return true;
|
||||||
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem
|
if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem)
|
||||||
&& eqItem.getEquipmentSlot() == eq) return true;
|
return eqItem.getEquipmentSlot() == eq;
|
||||||
return s.canEquip(eq, null);
|
return false;
|
||||||
}
|
}
|
||||||
@Override public void setChanged() {
|
@Override public void setChanged() {
|
||||||
super.setChanged();
|
super.setChanged();
|
||||||
@@ -220,13 +213,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
|
|||||||
this(id, inv, resolveTile(inv, buf));
|
this(id, inv, resolveTile(inv, buf));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 临时诊断:确认客户端能否拿到 tile(定位后删除) */
|
|
||||||
private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) {
|
private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) {
|
||||||
var pos = buf.readBlockPos();
|
var pos = buf.readBlockPos();
|
||||||
var be = inv.player.level().getBlockEntity(pos);
|
return (TileArcaneAssembler) inv.player.level().getBlockEntity(pos);
|
||||||
ThaumicEnergistics.LOG.info("[AA-DEBUG] client ctor: pos={}, be={}",
|
|
||||||
pos, be == null ? "null" : be.getClass().getSimpleName());
|
|
||||||
return (TileArcaneAssembler) be;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ItemStackHandler wrapMachineInventory() {
|
private ItemStackHandler wrapMachineInventory() {
|
||||||
|
|||||||
@@ -33,9 +33,11 @@ import appeng.me.storage.LinkStatusRespectingInventory;
|
|||||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||||
import thaumcraft.api.aspects.Aspect;
|
import thaumcraft.api.aspects.Aspect;
|
||||||
import thaumcraft.api.aspects.AspectList;
|
import thaumcraft.api.aspects.AspectList;
|
||||||
|
import thaumcraft.api.wands.WandCap;
|
||||||
import net.minecraft.world.item.crafting.Ingredient;
|
import net.minecraft.world.item.crafting.Ingredient;
|
||||||
import thaumcraft.common.items.TCFunctionalItems;
|
import thaumcraft.common.items.TCFunctionalItems;
|
||||||
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
|
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
|
||||||
|
import thaumcraft.common.vis.TCVisHelper;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
|
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
|
||||||
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
|
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
|
||||||
@@ -55,6 +57,34 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
*/
|
*/
|
||||||
private boolean isCraftingResultBeingTaken = false;
|
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 ISegmentedInventory craftingInventoryHost;
|
||||||
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
|
private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9];
|
||||||
private final ArcaneCraftingResultSlot resultSlot;
|
private final ArcaneCraftingResultSlot resultSlot;
|
||||||
@@ -76,9 +106,9 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
}
|
}
|
||||||
|
|
||||||
var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus);
|
var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus);
|
||||||
this.resultSlot = new ArcaneCraftingResultSlot(getPlayerInventory().player, craftingGridInv);
|
this.resultSlot = new ArcaneCraftingResultSlot(
|
||||||
this.resultSlot.setRefreshCallback(() -> updateCurrentRecipeAndOutput(false));
|
getPlayerInventory().player, this, craftingGridInv, craftingGridInv,
|
||||||
this.resultSlot.setContainer(this);
|
linkStatusInventory, this.getEnergySource(), this.getActionSource());
|
||||||
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
|
this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT);
|
||||||
|
|
||||||
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
|
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
|
||||||
@@ -88,7 +118,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mayPlace(ItemStack stack) {
|
public boolean mayPlace(ItemStack stack) {
|
||||||
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
|
//只允许 TCFunctionalItems.WandCastingItem 类型的物品放入
|
||||||
@@ -145,6 +174,26 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
|
|
||||||
private static final int CENTIVIS = 100;
|
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) {
|
private boolean hasEnoughVis(AspectList cost) {
|
||||||
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
|
ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost);
|
||||||
if (cost == null || cost.isEmpty()) {
|
if (cost == null || cost.isEmpty()) {
|
||||||
@@ -172,7 +221,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
|
private void updateCurrentRecipeAndOutput(boolean forceUpdate) {
|
||||||
ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate);
|
|
||||||
var player = getPlayerInventory().player;
|
var player = getPlayerInventory().player;
|
||||||
var level = player.level();
|
var level = player.level();
|
||||||
|
|
||||||
@@ -183,27 +231,20 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
|
var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer();
|
||||||
|
|
||||||
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
|
ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player);
|
||||||
ThaumicEnergistics.LOG.debug("[R] findMatchingArcaneRecipe result={}", arcaneResult);
|
|
||||||
|
|
||||||
if (!arcaneResult.isEmpty()) {
|
if (!arcaneResult.isEmpty()) {
|
||||||
requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
|
requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player);
|
||||||
ThaumicEnergistics.LOG.debug("[R] ARCANE MATCH! aspects={}, result={}", requiredAspects, arcaneResult);
|
|
||||||
foundArcane = true;
|
foundArcane = true;
|
||||||
|
|
||||||
if (hasEnoughVis(requiredAspects)) {
|
if (hasEnoughVis(requiredAspects)) {
|
||||||
ThaumicEnergistics.LOG.debug("[R] Vis OK → showing result");
|
|
||||||
resultSlot.setDisplayedCraftingOutput(arcaneResult);
|
resultSlot.setDisplayedCraftingOutput(arcaneResult);
|
||||||
} else {
|
} else {
|
||||||
ThaumicEnergistics.LOG.debug("[R] Vis INSUFFICIENT → result empty, but recipe recognized");
|
|
||||||
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
|
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ThaumicEnergistics.LOG.debug("[R] No arcane recipe match");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!foundArcane) {
|
if (!foundArcane) {
|
||||||
ThaumicEnergistics.LOG.debug("[R] No arcane match, trying vanilla...");
|
|
||||||
var testItems = new ArrayList<ItemStack>(this.craftingSlots.length);
|
var testItems = new ArrayList<ItemStack>(this.craftingSlots.length);
|
||||||
for (var craftingSlot : this.craftingSlots) {
|
for (var craftingSlot : this.craftingSlots) {
|
||||||
testItems.add(craftingSlot.getItem().copy());
|
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);
|
var recipeHolder = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, testInput, level);
|
||||||
if (recipeHolder.isPresent()) {
|
if (recipeHolder.isPresent()) {
|
||||||
ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess());
|
ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess());
|
||||||
ThaumicEnergistics.LOG.debug("[R] Vanilla match: {}, setting result slot", result);
|
|
||||||
resultSlot.setDisplayedCraftingOutput(result);
|
resultSlot.setDisplayedCraftingOutput(result);
|
||||||
} else {
|
} else {
|
||||||
ThaumicEnergistics.LOG.debug("[R] No vanilla match either, clearing result slot");
|
|
||||||
// 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态
|
// 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态
|
||||||
if (!level.isClientSide()) {
|
if (!level.isClientSide()) {
|
||||||
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
|
resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY);
|
||||||
@@ -225,9 +264,7 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
|
|
||||||
if (foundArcane) {
|
if (foundArcane) {
|
||||||
// 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值
|
// 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值
|
||||||
ThaumicEnergistics.LOG.debug("[R] foundArcane branch: isClientSide={}", level.isClientSide());
|
|
||||||
if (level.isClientSide()) {
|
if (level.isClientSide()) {
|
||||||
ThaumicEnergistics.LOG.debug("[R] Client: keeping server-synced values, not overriding");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.hasValidRecipe = true;
|
this.hasValidRecipe = true;
|
||||||
@@ -235,32 +272,27 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
|
|||||||
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
|
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
|
||||||
var ws = wandInv.getStackInSlot(0);
|
var ws = wandInv.getStackInSlot(0);
|
||||||
resultSlot.setWandStack(ws);
|
resultSlot.setWandStack(ws);
|
||||||
ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=true, wandStack={}", ws);
|
|
||||||
|
|
||||||
if (requiredAspects != null && requiredAspects.size() > 0) {
|
if (requiredAspects != null && requiredAspects.size() > 0) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (Map.Entry<Aspect, Integer> entry : requiredAspects.aspects.entrySet()) {
|
for (Map.Entry<Aspect, Integer> entry : requiredAspects.aspects.entrySet()) {
|
||||||
if (!sb.isEmpty()) sb.append(",");
|
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();
|
this.requiredAspectsData = sb.toString();
|
||||||
ThaumicEnergistics.LOG.debug("[R] aspectsData={}", this.requiredAspectsData);
|
|
||||||
} else {
|
} else {
|
||||||
this.requiredAspectsData = "";
|
this.requiredAspectsData = "";
|
||||||
ThaumicEnergistics.LOG.debug("[R] aspectsData=empty (no aspects)");
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ThaumicEnergistics.LOG.debug("[R] else branch: isClientSide={}, levelClass={}",
|
|
||||||
level.isClientSide(), level.getClass().getSimpleName());
|
|
||||||
if (level.isClientSide()) {
|
if (level.isClientSide()) {
|
||||||
ThaumicEnergistics.LOG.debug("[R] Client: not overriding synced values (no arcane recipe on client)");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.requiredAspectsData = "";
|
this.requiredAspectsData = "";
|
||||||
this.hasValidRecipe = false;
|
this.hasValidRecipe = false;
|
||||||
resultSlot.setRequiredAspects(null);
|
resultSlot.setRequiredAspects(null);
|
||||||
resultSlot.setWandStack(ItemStack.EMPTY);
|
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.TCResearchManager;
|
||||||
import thaumcraft.common.research.ThaumometerScanManager;
|
import thaumcraft.common.research.ThaumometerScanManager;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
|
import thaumicenergistics.common.container.slot.ReadOnlySlot;
|
||||||
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
|
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
|
||||||
import thaumicenergistics.common.integration.tc.TCReflection;
|
import thaumicenergistics.common.integration.tc.TCReflection;
|
||||||
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
|
import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket;
|
||||||
@@ -98,20 +99,10 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
|
|||||||
|
|
||||||
for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) {
|
for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) {
|
||||||
int y = SLOT_ASPECTS_Y + i * 18;
|
int y = SLOT_ASPECTS_Y + i * 18;
|
||||||
addSlot(new SlotItemHandler(aspectInventory, i, SLOT_ASPECTS_X, y) {
|
addSlot(new ReadOnlySlot(aspectInventory, i, SLOT_ASPECTS_X, y));
|
||||||
@Override
|
|
||||||
public boolean mayPlace(ItemStack s) { return false; }
|
|
||||||
@Override
|
|
||||||
public boolean mayPickup(Player p) { return false; }
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addSlot(new SlotItemHandler(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y) {
|
addSlot(new ReadOnlySlot(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y));
|
||||||
@Override
|
|
||||||
public boolean mayPlace(ItemStack s) { return false; }
|
|
||||||
@Override
|
|
||||||
public boolean mayPickup(Player p) { return false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (encoder != null) {
|
if (encoder != null) {
|
||||||
// 源物品槽:ghost 模板(背包拖入不消耗——实际物品还在手里;空手点击清空;JEI 拖拽设置模板)
|
// 源物品槽: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.api.crafting.ShapelessArcaneRecipe;
|
||||||
import thaumcraft.common.research.TCResearchManager;
|
import thaumcraft.common.research.TCResearchManager;
|
||||||
import thaumicenergistics.common.container.slot.GhostSlot;
|
import thaumicenergistics.common.container.slot.GhostSlot;
|
||||||
|
import thaumicenergistics.common.container.slot.ReadOnlySlot;
|
||||||
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
|
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
|
||||||
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
|
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
|
||||||
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
|
import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket;
|
||||||
@@ -121,12 +122,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
|
|||||||
initPatternSlots();
|
initPatternSlots();
|
||||||
initCraftingSlots();
|
initCraftingSlots();
|
||||||
|
|
||||||
addSlot(new SlotItemHandler(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y) {
|
addSlot(new ReadOnlySlot(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y));
|
||||||
@Override
|
|
||||||
public boolean mayPlace(ItemStack s) { return false; }
|
|
||||||
@Override
|
|
||||||
public boolean mayPickup(Player p) { return false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
saveStateSlot = addDataSlot(DataSlot.standalone());
|
saveStateSlot = addDataSlot(DataSlot.standalone());
|
||||||
saveStateSlot.set(0);
|
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_<tag>),与 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.entity.player.Inventory;
|
||||||
import net.minecraft.world.inventory.ContainerData;
|
import net.minecraft.world.inventory.ContainerData;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
|
import org.anti_ad.mc.ipn.api.IPNIgnore;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 奥术装配器 GUI。
|
* 奥术装配器 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
|
||||||
*
|
*
|
||||||
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
|
* 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域:
|
||||||
* 第1块 主矩形:(0,0) → 176×197
|
* 第1块 主矩形:(0,0) → 176×197
|
||||||
@@ -20,6 +21,7 @@ import thaumicenergistics.ThaumicEnergistics;
|
|||||||
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
|
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
|
||||||
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
|
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
|
||||||
*/
|
*/
|
||||||
|
@IPNIgnore
|
||||||
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
|
public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneAssembler> {
|
||||||
|
|
||||||
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
|
static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
|||||||
@@ -11,17 +11,13 @@ import appeng.client.gui.style.ScreenStyle;
|
|||||||
|
|
||||||
import java.util.Map;
|
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 = {
|
* 奥术合成终端 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。
|
||||||
{10, 95}, // 左列 第1行
|
*/
|
||||||
{10, 112}, // 左列 第2行
|
@IPNIgnore
|
||||||
{10, 128}, // 左列 第3行
|
public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCraftingTerminal> {
|
||||||
{80, 95}, // 右列 第1行
|
|
||||||
{80, 112}, // 右列 第2行
|
|
||||||
{80, 128}, // 右列 第3行
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final int ICON_SIZE = 14;
|
private static final int ICON_SIZE = 14;
|
||||||
|
|
||||||
@@ -30,9 +26,6 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen<ContainerArcaneCr
|
|||||||
super(menu, playerInventory, title, style);
|
super(menu, playerInventory, title, style);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 合成结果槽锚点 Y(首次计算缓存,避免每帧遍历 slots)。 */
|
|
||||||
private int cachedAnchorY = -1;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
|
public void drawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY) {
|
||||||
super.drawFG(guiGraphics, offsetX, offsetY, mouseX, 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();
|
Map<String, Integer> aspectMap = this.menu.getParsedAspects();
|
||||||
if (aspectMap.isEmpty()) return;
|
if (aspectMap.isEmpty()) return;
|
||||||
|
|
||||||
// 获取合成结果槽的 Y 位置作为锚点(槽位置不变,首次计算后缓存,避免每帧遍历)
|
// 锚点取合成结果槽的 Y:圆环紧贴结果槽左侧,窗口缩放时随槽位一起移动
|
||||||
int anchorY;
|
int anchorY = -1;
|
||||||
if (cachedAnchorY < 0) {
|
|
||||||
for (var slot : this.menu.slots) {
|
for (var slot : this.menu.slots) {
|
||||||
// CRAFTING_RESULT 槽在 style JSON 中 bottom:140
|
|
||||||
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
|
if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) {
|
||||||
cachedAnchorY = slot.y;
|
anchorY = slot.y;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 若找不到锚点则回退到硬编码值
|
if (anchorY < 0) anchorY = 95;
|
||||||
if (cachedAnchorY < 0) cachedAnchorY = 95;
|
|
||||||
}
|
|
||||||
anchorY = cachedAnchorY;
|
|
||||||
|
|
||||||
// 要素圆圈 Y = 锚点 + 偏移(-16/1/17,对应纹理 95/112/128)
|
// 两列三行:左列 x=10,右列 x=80;行偏移对应三行圆环
|
||||||
int[] aspectOffsets = { -16, 1, 17 };
|
int[] aspectOffsets = { -16, 1, 17 };
|
||||||
|
|
||||||
int index = 0;
|
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 net.minecraft.world.item.ItemStack;
|
||||||
|
|
||||||
import appeng.api.inventories.InternalInventory;
|
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.Aspect;
|
||||||
import thaumcraft.api.aspects.AspectList;
|
import thaumcraft.api.aspects.AspectList;
|
||||||
import thaumcraft.common.items.TCFunctionalItems;
|
import thaumcraft.common.items.TCFunctionalItems;
|
||||||
import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager;
|
|
||||||
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
|
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 奥术合成结果槽:在取走合成结果时消耗法杖要素。
|
* 奥术合成结果槽:继承 AE2 CraftingTermSlot 使客户端识别(shift/右键/space 合成自动生效),
|
||||||
* 参考 TC4 1.21.1 ArcaneWorkbenchMenu.onResultTaken + consumeVis。
|
* 并保留 vis 消耗检查(mayPickup)与取走时 vis 扣除 + 自动补货(onTake)。
|
||||||
*/
|
*/
|
||||||
public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
|
public class ArcaneCraftingResultSlot extends CraftingTermSlot {
|
||||||
|
|
||||||
/** TC4 内部使用 centivis,1 vis = 100 centivis */
|
/** TC4 内部使用 centivis,1 vis = 100 centivis */
|
||||||
private static final int CENTIVIS = 100;
|
private static final int CENTIVIS = 100;
|
||||||
|
|
||||||
private AspectList requiredAspects = null;
|
private AspectList requiredAspects = null;
|
||||||
private ItemStack wandStack = ItemStack.EMPTY;
|
private ItemStack wandStack = ItemStack.EMPTY;
|
||||||
private Runnable refreshCallback = null;
|
|
||||||
private ContainerArcaneCraftingTerminal container = 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) {
|
public ArcaneCraftingResultSlot(Player player, ContainerArcaneCraftingTerminal container,
|
||||||
super(player, craftingGridInv);
|
InternalInventory craftingGridInv, InternalInventory secondMatrix,
|
||||||
}
|
MEStorage storage, IEnergySource energySrc, IActionSource mySrc) {
|
||||||
|
super(player, mySrc, energySrc, storage, craftingGridInv, secondMatrix, (ICraftingGridMenu) container);
|
||||||
public void setContainer(ContainerArcaneCraftingTerminal container) {
|
|
||||||
this.container = container;
|
this.container = container;
|
||||||
}
|
this.storage = storage;
|
||||||
|
this.energySrc = energySrc;
|
||||||
public void setRefreshCallback(Runnable callback) {
|
this.mySrc = mySrc;
|
||||||
this.refreshCallback = callback;
|
this.craftingGrid = craftingGridInv;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRequiredAspects(AspectList aspects) {
|
public void setRequiredAspects(AspectList aspects) {
|
||||||
@@ -47,40 +54,177 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean mayPickup(Player player) {
|
public boolean mayPickup(Player player) {
|
||||||
if (requiredAspects == null || requiredAspects.isEmpty()) {
|
// AE2 原版:结果槽不可 vanilla 点按取走,所有取走走 doClick(含 vis 检查)
|
||||||
return super.mayPickup(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wandStack.isEmpty()) return false;
|
|
||||||
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
|
|
||||||
return false;
|
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();
|
AspectList centivisCost = new AspectList();
|
||||||
for (Aspect aspect : requiredAspects.aspects()) {
|
for (Aspect aspect : requiredAspects.aspects()) {
|
||||||
centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS);
|
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
|
@Override
|
||||||
public void onTake(Player player, ItemStack stack) {
|
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) {
|
if (container != null) {
|
||||||
container.setCraftingResultBeingTaken(true);
|
container.setCraftingResultBeingTaken(true);
|
||||||
}
|
}
|
||||||
try {
|
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);
|
super.onTake(player, stack);
|
||||||
|
|
||||||
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
|
if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) {
|
||||||
@@ -93,28 +237,22 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 ME 网络自动补充消耗掉的合成材料
|
// 自动补货:合成格材料被消耗后从 ME 存储补回
|
||||||
if (container != null && !beforeItems.isEmpty()) {
|
if (!beforeItems.isEmpty()) {
|
||||||
var grid = container.getCraftingMatrix();
|
if (storage != null && energySrc != null && mySrc != null) {
|
||||||
var node = container.getGridNode();
|
|
||||||
if (node != null && node.getGrid() != null) {
|
|
||||||
var storage = node.getGrid().getStorageService().getInventory();
|
|
||||||
var energy = container.getEnergySource();
|
|
||||||
var actionSource = container.getActionSource();
|
|
||||||
for (var entry : beforeItems.entrySet()) {
|
for (var entry : beforeItems.entrySet()) {
|
||||||
int slot = entry.getKey();
|
int slot = entry.getKey();
|
||||||
var beforeItem = entry.getValue();
|
var beforeItem = entry.getValue();
|
||||||
var currentItem = grid.getStackInSlot(slot);
|
var currentItem = grid.getStackInSlot(slot);
|
||||||
// 如果该格物品变少了(被消耗了),从网络提取补充
|
|
||||||
if (currentItem.getCount() < beforeItem.getCount()) {
|
if (currentItem.getCount() < beforeItem.getCount()) {
|
||||||
long needed = beforeItem.getCount() - currentItem.getCount();
|
long needed = beforeItem.getCount() - currentItem.getCount();
|
||||||
var key = appeng.api.stacks.AEItemKey.of(beforeItem);
|
var key = appeng.api.stacks.AEItemKey.of(beforeItem);
|
||||||
if (key != null) {
|
if (key != null) {
|
||||||
long extracted = appeng.api.storage.StorageHelper.poweredExtraction(
|
long extracted = appeng.api.storage.StorageHelper.poweredExtraction(
|
||||||
energy, storage, key, needed, actionSource);
|
energySrc, storage, key, needed, mySrc);
|
||||||
if (extracted > 0) {
|
if (extracted > 0) {
|
||||||
var toPlace = beforeItem.copyWithCount((int) (currentItem.getCount() + extracted));
|
grid.setItemDirect(slot, beforeItem.copyWithCount(
|
||||||
grid.setItemDirect(slot, toPlace);
|
(int) (currentItem.getCount() + extracted)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,8 +264,6 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot {
|
|||||||
container.setCraftingResultBeingTaken(false);
|
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",
|
"lBl",
|
||||||
"QPQ",
|
"QPQ",
|
||||||
'Q', ae("quartz_glass"),
|
'Q', ae("quartz_glass"),
|
||||||
'l', new ItemStack(Items.LAPIS_LAZULI, 4),
|
'l', new ItemStack(Items.LAPIS_LAZULI, 1),
|
||||||
'B', tc("zombie_brain"),
|
'B', tc("zombie_brain"),
|
||||||
'P', ae("calculation_processor"));
|
'P', ae("calculation_processor"));
|
||||||
|
|
||||||
@@ -138,7 +138,7 @@ public final class RecipeRegistration {
|
|||||||
"qWq",
|
"qWq",
|
||||||
"FqA",
|
"FqA",
|
||||||
'A', tc("air_shard"),
|
'A', tc("air_shard"),
|
||||||
'q', new ItemStack(Items.QUARTZ, 4),
|
'q', new ItemStack(Items.QUARTZ, 1),
|
||||||
'F', tc("fire_shard"),
|
'F', tc("fire_shard"),
|
||||||
'W', ae("certus_quartz_wrench"));
|
'W', ae("certus_quartz_wrench"));
|
||||||
|
|
||||||
@@ -156,22 +156,23 @@ public final class RecipeRegistration {
|
|||||||
|
|
||||||
ESSENTIA_IMPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_IMPORT_BUS),
|
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),
|
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
|
||||||
"JDJ",
|
// 注意:TC ShapedArcaneRecipe 要求模式为矩形(各行等长),删 J 后必须补空格占位
|
||||||
|
"JD ",
|
||||||
"IFI",
|
"IFI",
|
||||||
" ",
|
" ",
|
||||||
'J', tc("warded_jar"),
|
'J', tc("jar_of_essentia"),
|
||||||
'D', stack(ModItems.DIFFUSION_CORE),
|
'D', stack(ModItems.DIFFUSION_CORE),
|
||||||
'I', new ItemStack(Items.IRON_INGOT, 2),
|
'I', new ItemStack(Items.IRON_INGOT, 1),
|
||||||
'F', tc("essentia_filter"));
|
'F', tc("essentia_filter"));
|
||||||
|
|
||||||
ESSENTIA_EXPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_EXPORT_BUS),
|
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),
|
new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1),
|
||||||
"JCJ",
|
"JC ",
|
||||||
"IFI",
|
"IFI",
|
||||||
" ",
|
" ",
|
||||||
'J', tc("warded_jar"),
|
'J', tc("jar_of_essentia"),
|
||||||
'C', stack(ModItems.COALESCENCE_CORE),
|
'C', stack(ModItems.COALESCENCE_CORE),
|
||||||
'I', new ItemStack(Items.IRON_INGOT, 2),
|
'I', new ItemStack(Items.IRON_INGOT, 1),
|
||||||
'F', tc("essentia_filter"));
|
'F', tc("essentia_filter"));
|
||||||
|
|
||||||
ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS),
|
ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS),
|
||||||
@@ -182,7 +183,7 @@ public final class RecipeRegistration {
|
|||||||
'D', stack(ModItems.DIFFUSION_CORE),
|
'D', stack(ModItems.DIFFUSION_CORE),
|
||||||
'C', stack(ModItems.COALESCENCE_CORE),
|
'C', stack(ModItems.COALESCENCE_CORE),
|
||||||
'F', tc("essentia_filter"),
|
'F', tc("essentia_filter"),
|
||||||
'I', new ItemStack(Items.IRON_INGOT, 2),
|
'I', new ItemStack(Items.IRON_INGOT, 1),
|
||||||
'G', ae("quartz_glass"));
|
'G', ae("quartz_glass"));
|
||||||
|
|
||||||
KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
|
KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
|
||||||
@@ -190,7 +191,7 @@ public final class RecipeRegistration {
|
|||||||
"ILI",
|
"ILI",
|
||||||
" T ",
|
" T ",
|
||||||
"IPI",
|
"IPI",
|
||||||
'I', new ItemStack(Items.IRON_INGOT, 4),
|
'I', new ItemStack(Items.IRON_INGOT, 1),
|
||||||
'L', MONITORS,
|
'L', MONITORS,
|
||||||
'T', tc("thaumonomicon"),
|
'T', tc("thaumonomicon"),
|
||||||
'P', ae("logic_processor"));
|
'P', ae("logic_processor"));
|
||||||
@@ -200,7 +201,7 @@ public final class RecipeRegistration {
|
|||||||
"ILI",
|
"ILI",
|
||||||
" G ",
|
" G ",
|
||||||
"IPI",
|
"IPI",
|
||||||
'I', new ItemStack(Items.IRON_INGOT, 4),
|
'I', new ItemStack(Items.IRON_INGOT, 1),
|
||||||
'L', MONITORS,
|
'L', MONITORS,
|
||||||
'G', tc("thaumometer"),
|
'G', tc("thaumometer"),
|
||||||
'P', ae("engineering_processor"));
|
'P', ae("engineering_processor"));
|
||||||
@@ -252,7 +253,7 @@ public final class RecipeRegistration {
|
|||||||
// 6. 源流接口
|
// 6. 源流接口
|
||||||
VIS_INTERFACE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("VISINTERFACE", stack(ModItems.VIS_INTERFACE),
|
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),
|
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"));
|
ae("me_p2p_tunnel"));
|
||||||
|
|
||||||
// 7. 原质振荡室
|
// 7. 原质振荡室
|
||||||
@@ -293,27 +294,38 @@ public final class RecipeRegistration {
|
|||||||
tc("salis_mundus"));
|
tc("salis_mundus"));
|
||||||
|
|
||||||
// 3. 奥术装配器
|
// 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),
|
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),
|
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"),
|
ae("molecular_assembler"),
|
||||||
tc("fire_shard"),
|
tc("fire_shard"),
|
||||||
tc("entropy_shard"),
|
tc("entropy_shard"),
|
||||||
tc("order_shard"),
|
tc("order_shard"),
|
||||||
tc("balance_shard"),
|
tc("balanced_shard"),
|
||||||
tc("water_shard"),
|
tc("water_shard"),
|
||||||
tc("earth_shard"),
|
tc("earth_shard"),
|
||||||
tc("air_shard"),
|
tc("air_shard"),
|
||||||
tc("salis_mundus"),
|
|
||||||
createFullVisScepter());
|
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() {
|
private static ItemStack createFullVisScepter() {
|
||||||
ItemStack scepter = tc("silverwood_thaumium_scepter");
|
ItemStack scepter = tc("silverwood_thaumium_scepter");
|
||||||
if (scepter.isEmpty()) return scepter;
|
if (scepter.isEmpty()) return scepter;
|
||||||
TCVisHelper.setWandParts(scepter, "silverwood", "thaumium", true);
|
|
||||||
int max = TCVisHelper.getWandMaxVis(scepter, "silverwood_thaumium_scepter");
|
int max = TCVisHelper.getWandMaxVis(scepter, "silverwood_thaumium_scepter");
|
||||||
for (Aspect a : Aspect.getPrimalAspects()) {
|
for (Aspect a : Aspect.getPrimalAspects()) {
|
||||||
TCVisHelper.storeVis(scepter, a, max, max);
|
TCVisHelper.storeVis(scepter, a, max, max);
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import java.util.Map;
|
|||||||
|
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
import appeng.api.config.Actionable;
|
||||||
import appeng.api.networking.IGrid;
|
import appeng.api.networking.IGrid;
|
||||||
import appeng.api.networking.IGridServiceProvider;
|
import appeng.api.networking.IGridServiceProvider;
|
||||||
import appeng.api.networking.security.IActionSource;
|
import appeng.api.networking.security.IActionSource;
|
||||||
import appeng.api.networking.storage.IStorageService;
|
import appeng.api.networking.storage.IStorageService;
|
||||||
import appeng.api.storage.MEStorage;
|
import appeng.api.storage.MEStorage;
|
||||||
import appeng.api.storage.StorageHelper;
|
import appeng.api.storage.StorageHelper;
|
||||||
import appeng.api.networking.security.IActionSource;
|
|
||||||
import thaumicenergistics.api.grid.IEssentiaGrid;
|
import thaumicenergistics.api.grid.IEssentiaGrid;
|
||||||
import thaumicenergistics.api.storage.AspectStack;
|
import thaumicenergistics.api.storage.AspectStack;
|
||||||
import thaumicenergistics.api.storage.IAspectStack;
|
import thaumicenergistics.api.storage.IAspectStack;
|
||||||
@@ -101,11 +101,28 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider
|
|||||||
@Override
|
@Override
|
||||||
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
|
public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) {
|
||||||
if (amount <= 0) return 0;
|
if (amount <= 0) return 0;
|
||||||
|
// 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化)
|
||||||
|
var nativeInjected = injectNative(aspectId, amount, simulate);
|
||||||
|
if (nativeInjected < amount) {
|
||||||
|
// 原生存储空间不足时剩余部分退回内存 monitor(供注魔供应器等内部读取)
|
||||||
|
long remaining = amount - nativeInjected;
|
||||||
if (!simulate) {
|
if (!simulate) {
|
||||||
long current = monitor.get(aspectId);
|
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
|
@Override
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
|
|||||||
CompoundTag data = accessor.getServerData();
|
CompoundTag data = accessor.getServerData();
|
||||||
if (data == null || data.isEmpty()) return;
|
if (data == null || data.isEmpty()) return;
|
||||||
|
|
||||||
// 吸力类型
|
// 当前仓内源质类型(通配吸力模式下显示仓内实际燃料)
|
||||||
if (data.contains(TAG_ASPECT)) {
|
if (data.contains(TAG_ASPECT)) {
|
||||||
String aspect = data.getString(TAG_ASPECT);
|
String aspect = data.getString(TAG_ASPECT);
|
||||||
String aspectKey = "thaumicenergistics.aspect." + 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)));
|
Component.translatable(aspectKey)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,8 +93,8 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer
|
|||||||
data.putDouble(TAG_ENERGY_MAX, chamber.getMaxEnergyStorage());
|
data.putDouble(TAG_ENERGY_MAX, chamber.getMaxEnergyStorage());
|
||||||
data.putDouble(TAG_ENERGY_OUTPUT, chamber.getMaxOutputPerTick());
|
data.putDouble(TAG_ENERGY_OUTPUT, chamber.getMaxOutputPerTick());
|
||||||
data.putBoolean(TAG_GENERATING, chamber.isActive());
|
data.putBoolean(TAG_GENERATING, chamber.isActive());
|
||||||
if (chamber.getSuctionType(null) != null) {
|
if (chamber.getEssentiaType(null) != null) {
|
||||||
data.putString(TAG_ASPECT, chamber.getSuctionType(null).getTag());
|
data.putString(TAG_ASPECT, chamber.getEssentiaType(null).getTag());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import net.minecraft.world.phys.BlockHitResult;
|
|||||||
import appeng.helpers.WirelessTerminalMenuHost;
|
import appeng.helpers.WirelessTerminalMenuHost;
|
||||||
import appeng.items.tools.powered.WirelessTerminalItem;
|
import appeng.items.tools.powered.WirelessTerminalItem;
|
||||||
import appeng.menu.locator.ItemMenuHostLocator;
|
import appeng.menu.locator.ItemMenuHostLocator;
|
||||||
import appeng.menu.me.common.MEStorageMenu;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无线源质终端物品。继承 AE2 的WirelessTerminalItem
|
* 无线源质终端物品。继承 AE2 的WirelessTerminalItem
|
||||||
@@ -25,7 +24,7 @@ public class WirelessEssentiaTerminalItem extends WirelessTerminalItem {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MenuType<?> getMenuType() {
|
public MenuType<?> getMenuType() {
|
||||||
return MEStorageMenu.WIRELESS_TYPE;
|
return thaumicenergistics.init.ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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)
|
(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(服务端→客户端,研究未解锁拦截提示,携带研究键)
|
// ResearchGateErrorPacket — S2C(服务端→客户端,研究未解锁拦截提示,携带研究键)
|
||||||
registrar.playToClient(
|
registrar.playToClient(
|
||||||
ResearchGateErrorPacket.TYPE,
|
ResearchGateErrorPacket.TYPE,
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ public class EssentiaExportBusPart extends IOBusPart
|
|||||||
|
|
||||||
private static final double IDLE_POWER = 0.5;
|
private static final double IDLE_POWER = 0.5;
|
||||||
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA;
|
private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA;
|
||||||
private static final int TRANSFER_RATE = 8;
|
|
||||||
|
|
||||||
private boolean isWorkingFlag = false;
|
private boolean isWorkingFlag = false;
|
||||||
|
|
||||||
@@ -70,6 +69,14 @@ public class EssentiaExportBusPart extends IOBusPart
|
|||||||
|
|
||||||
private long storedEnergy = 0;
|
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) {
|
public EssentiaExportBusPart(IPartItem<?> partItem) {
|
||||||
super(TickRates.ExportBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
|
super(TickRates.ExportBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
|
||||||
this.getMainNode().setIdlePowerUsage(IDLE_POWER);
|
this.getMainNode().setIdlePowerUsage(IDLE_POWER);
|
||||||
@@ -122,7 +129,7 @@ public class EssentiaExportBusPart extends IOBusPart
|
|||||||
if (key == null) return false;
|
if (key == null) return false;
|
||||||
|
|
||||||
long extracted = grid.getStorageService().getInventory().extract(
|
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;
|
if (extracted <= 0) return false;
|
||||||
|
|
||||||
// 尝试注入到相邻方块:先 IAspectContainer,再回退到 IEssentiaTransport
|
// 尝试注入到相邻方块:先 IAspectContainer,再回退到 IEssentiaTransport
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ public class EssentiaImportBusPart extends IOBusPart
|
|||||||
|
|
||||||
private boolean isWorkingFlag = false;
|
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(
|
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
|
||||||
selection -> { },
|
selection -> { },
|
||||||
keyType -> keyType == AEssentiaKeyType.INSTANCE);
|
keyType -> keyType == AEssentiaKeyType.INSTANCE);
|
||||||
@@ -104,7 +112,7 @@ public class EssentiaImportBusPart extends IOBusPart
|
|||||||
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
|
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
|
||||||
if (adjacentBE == null) return false;
|
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;
|
if (extracted == null || extracted.isEmpty()) return false;
|
||||||
|
|
||||||
long injected = this.injectToNetwork(grid, extracted);
|
long injected = this.injectToNetwork(grid, extracted);
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import appeng.api.parts.IPartItem;
|
|||||||
import appeng.api.parts.IPartModel;
|
import appeng.api.parts.IPartModel;
|
||||||
import appeng.api.util.KeyTypeSelection;
|
import appeng.api.util.KeyTypeSelection;
|
||||||
import appeng.items.parts.PartModels;
|
import appeng.items.parts.PartModels;
|
||||||
import appeng.menu.me.common.MEStorageMenu;
|
|
||||||
import appeng.parts.PartModel;
|
import appeng.parts.PartModel;
|
||||||
import appeng.parts.reporting.AbstractTerminalPart;
|
import appeng.parts.reporting.AbstractTerminalPart;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
@@ -54,7 +53,7 @@ public class EssentiaTerminalPart extends AbstractTerminalPart {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MenuType<?> getMenuType(Player player) {
|
public MenuType<?> getMenuType(Player player) {
|
||||||
return MEStorageMenu.TYPE;
|
return thaumicenergistics.init.ModMenuTypes.ESSENTIA_TERMINAL.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
|
|||||||
/** 抽取颜色在没有新抽取后保持点亮的时间(ms),超时清除。 */
|
/** 抽取颜色在没有新抽取后保持点亮的时间(ms),超时清除。 */
|
||||||
private static final long TIME_TO_CLEAR = 500L;
|
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_UID = "uid";
|
||||||
private static final String NBT_KEY_IS_PROVIDER = "isProvider";
|
private static final String NBT_KEY_IS_PROVIDER = "isProvider";
|
||||||
|
|
||||||
@@ -290,7 +293,7 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TickingRequest getTickingRequest(IGridNode node) {
|
public TickingRequest getTickingRequest(IGridNode node) {
|
||||||
return new TickingRequest(30, 30, false);
|
return new TickingRequest(TICK_RATE, TICK_RATE, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ import thaumcraft.api.aspects.AspectList;
|
|||||||
import thaumcraft.api.crafting.IArcaneRecipe;
|
import thaumcraft.api.crafting.IArcaneRecipe;
|
||||||
|
|
||||||
import thaumicenergistics.api.grid.IEssentiaGrid;
|
import thaumicenergistics.api.grid.IEssentiaGrid;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
|
||||||
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
|
import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern;
|
||||||
import thaumicenergistics.common.integration.tc.TCReflection;
|
import thaumicenergistics.common.integration.tc.TCReflection;
|
||||||
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
|
import thaumicenergistics.common.inventory.HandlerKnowledgeCore;
|
||||||
@@ -314,7 +313,6 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
|
|
||||||
boolean enoughVis = hasEnoughVisForCraft();
|
boolean enoughVis = hasEnoughVisForCraft();
|
||||||
if (!enoughVis) {
|
if (!enoughVis) {
|
||||||
ThaumicEnergistics.LOG.debug("[AA] craftingTick: vis 不足,等待补充 (storedVis cvis={})", storedVis.visSize());
|
|
||||||
return TickRateModulation.SAME;
|
return TickRateModulation.SAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +329,6 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
long inserted = storage.getInventory().insert(
|
long inserted = storage.getInventory().insert(
|
||||||
outputKey, output.getCount(), Actionable.SIMULATE, actionSource);
|
outputKey, output.getCount(), Actionable.SIMULATE, actionSource);
|
||||||
if (inserted < output.getCount()) {
|
if (inserted < output.getCount()) {
|
||||||
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 产物无法插入网络(模拟 inserted={}/{}),等待", inserted, output.getCount());
|
|
||||||
return TickRateModulation.SAME;
|
return TickRateModulation.SAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,7 +336,6 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
|
|
||||||
storage.getInventory().insert(
|
storage.getInventory().insert(
|
||||||
outputKey, output.getCount(), Actionable.MODULATE, actionSource);
|
outputKey, output.getCount(), Actionable.MODULATE, actionSource);
|
||||||
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 完成!插入产物 {}x{} 到网络", outputKey, output.getCount());
|
|
||||||
|
|
||||||
isCrafting = false;
|
isCrafting = false;
|
||||||
currentRecipe = null;
|
currentRecipe = null;
|
||||||
@@ -359,11 +355,8 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
double extracted = energy.extractAEPower(need, Actionable.MODULATE, PowerMultiplier.CONFIG);
|
double extracted = energy.extractAEPower(need, Actionable.MODULATE, PowerMultiplier.CONFIG);
|
||||||
if (extracted >= need * 0.9) {
|
if (extracted >= need * 0.9) {
|
||||||
craftTickCounter += ticksSinceLast;
|
craftTickCounter += ticksSinceLast;
|
||||||
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need);
|
|
||||||
// 节流同步合成进度到客户端(GUI 第7格进度条动画)
|
// 节流同步合成进度到客户端(GUI 第7格进度条动画)
|
||||||
maybeMarkVisDirty();
|
maybeMarkVisDirty();
|
||||||
} else {
|
|
||||||
ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,19 +439,13 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
ArcaneAssemblerPattern ap = buildArcanePattern(acp);
|
ArcaneAssemblerPattern ap = buildArcanePattern(acp);
|
||||||
if (ap != null) {
|
if (ap != null) {
|
||||||
result.add(ap);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) {
|
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) {
|
if (patternDetails instanceof ArcaneAssemblerPattern arcanePattern) {
|
||||||
return beginCrafting(arcanePattern);
|
return beginCrafting(arcanePattern);
|
||||||
}
|
}
|
||||||
@@ -490,9 +477,6 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
|
private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) {
|
||||||
AspectList aspects = arcanePattern.getAspects();
|
AspectList aspects = arcanePattern.getAspects();
|
||||||
ItemStack result = arcanePattern.getResultStack();
|
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 (isCrafting || !mainNode.isActive()) return false;
|
||||||
if (aspects == null || aspects.isEmpty() || result.isEmpty()) 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());
|
this.internalInventory.setItem(TARGET_SLOT, result.copy());
|
||||||
setChanged();
|
setChanged();
|
||||||
markForUpdate();
|
markForUpdate();
|
||||||
ThaumicEnergistics.LOG.info("[AA] beginCrafting: STARTED, result={}, aspects={}", result, aspects.visSize());
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -593,7 +576,6 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
// 守卫 getGrid()!=null:装配器未连 ME 网络(无相邻 cable)时 getGrid() 为 null,
|
// 守卫 getGrid()!=null:装配器未连 ME 网络(无相邻 cable)时 getGrid() 为 null,
|
||||||
// 此时 requestUpdate 内部 node.getGrid().getCraftingService() 会 NPE,导致 kCore 插入事务回滚 → "无法绑定"。
|
// 此时 requestUpdate 内部 node.getGrid().getCraftingService() 会 NPE,导致 kCore 插入事务回滚 → "无法绑定"。
|
||||||
if (level != null && !level.isClientSide() && mainNode.getGrid() != null) {
|
if (level != null && !level.isClientSide() && mainNode.getGrid() != null) {
|
||||||
ThaumicEnergistics.LOG.info("[AA] kCore slot changed → requestUpdate");
|
|
||||||
ICraftingProvider.requestUpdate(mainNode);
|
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 MAX_ESSENTIA = 64;
|
||||||
private static final int BASE_BURN_TICKS = 800;
|
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;
|
private static final int IDLE_POWER = 1;
|
||||||
|
|
||||||
// 能量槽配置:8KFE = 8000 AE,对外最大输出 400 AE/t
|
// 能量槽配置:16KFE = 16000 AE,对外最大输出 1000 AE/t
|
||||||
public static final double MAX_ENERGY_STORAGE = 8000.0;
|
public static final double MAX_ENERGY_STORAGE = 16000.0;
|
||||||
public static final double MAX_OUTPUT_PER_TICK = 400.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;
|
protected final IManagedGridNode mainNode;
|
||||||
private final IGridNodeListener<TileEssentiaVibrationChamber> nodeListener = new IGridNodeListener<>() {
|
private final IGridNodeListener<TileEssentiaVibrationChamber> nodeListener = new IGridNodeListener<>() {
|
||||||
@@ -62,7 +66,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
private double aePerTick;
|
private double aePerTick;
|
||||||
private int storedEssentia;
|
private int storedEssentia;
|
||||||
private Aspect currentAspect;
|
private Aspect currentAspect;
|
||||||
private Aspect suctionAspect = Aspect.ENERGY;
|
|
||||||
|
|
||||||
private double storedEnergy;
|
private double storedEnergy;
|
||||||
|
|
||||||
@@ -97,7 +100,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public TickingRequest getTickingRequest(IGridNode node) {
|
public TickingRequest getTickingRequest(IGridNode node) {
|
||||||
return new TickingRequest(10, 40, false);
|
return new TickingRequest(TICK_RATE_BURNING, TICK_RATE_IDLE, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,8 +180,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
int srcAmt = source.getEssentiaAmount(sourceSide);
|
int srcAmt = source.getEssentiaAmount(sourceSide);
|
||||||
if (!canOut) continue;
|
if (!canOut) continue;
|
||||||
if (srcType == null) continue;
|
if (srcType == null) continue;
|
||||||
String srcTag = srcType.getTag();
|
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
|
||||||
if (!srcTag.equals("ignis") && !srcTag.equals("potentia") && !srcTag.equals("energy")) continue;
|
|
||||||
|
|
||||||
if (space <= 0) return;
|
if (space <= 0) return;
|
||||||
|
|
||||||
@@ -187,7 +189,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
if (taken > 0) {
|
if (taken > 0) {
|
||||||
storedEssentia += taken;
|
storedEssentia += taken;
|
||||||
currentAspect = srcType;
|
currentAspect = srcType;
|
||||||
suctionAspect = srcType;
|
// suctionAspect 固定为 null(通配吸力),不锁定单一类型
|
||||||
setChanged();
|
setChanged();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -240,9 +242,25 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
private boolean startBurning() {
|
private boolean startBurning() {
|
||||||
if (currentAspect == null) currentAspect = Aspect.FIRE;
|
if (currentAspect == null) currentAspect = Aspect.FIRE;
|
||||||
|
|
||||||
boolean isPotentia = currentAspect != null && (currentAspect == Aspect.ENERGY || currentAspect.getTag().equals("potentia") || currentAspect.getTag().equals("energy"));
|
String tag = currentAspect.getTag();
|
||||||
int burnTime = isPotentia ? (int)(BASE_BURN_TICKS / 1.6f) : BASE_BURN_TICKS / 2;
|
boolean isPotentia = currentAspect == Aspect.ENERGY || tag.equals("potentia") || tag.equals("energy");
|
||||||
double power = isPotentia ? BASE_AE_PER_TICK * 1.6 : BASE_AE_PER_TICK;
|
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;
|
burnTicksRemaining = burnTime;
|
||||||
totalBurnTicks = 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 canInputFrom(Direction side) { return true; }
|
||||||
@Override public boolean canOutputTo(Direction side) { return true; }
|
@Override public boolean canOutputTo(Direction side) { return true; }
|
||||||
@Override public void setSuction(Aspect aspect, int amount) {
|
@Override public void setSuction(Aspect aspect, int amount) {
|
||||||
this.suctionAspect = aspect;
|
// 通配吸力:始终吸所有源质类型,忽略外部设置
|
||||||
}
|
}
|
||||||
@Override public Aspect getSuctionType(Direction side) {
|
@Override public Aspect getSuctionType(Direction side) {
|
||||||
return suctionAspect;
|
return null; // null = 通配吸力:管道可输送任意类型源质
|
||||||
}
|
}
|
||||||
@Override public int getSuctionAmount(Direction side) { return 128; }
|
@Override public int getSuctionAmount(Direction side) { return 128; }
|
||||||
|
|
||||||
private boolean isFuelAspect(Aspect aspect) {
|
private boolean isFuelAspect(Aspect aspect) {
|
||||||
if (aspect == null) return false;
|
// 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2)
|
||||||
String tag = aspect.getTag();
|
return aspect != null;
|
||||||
return tag.equals("ignis") || tag.equals("potentia") || tag.equals("energy");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -298,11 +315,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
@Override public boolean renderExtendedTube() { return false; }
|
@Override public boolean renderExtendedTube() { return false; }
|
||||||
|
|
||||||
public void setSuctionType(Aspect aspect) {
|
public void setSuctionType(Aspect aspect) {
|
||||||
if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) {
|
// 通配吸力模式下无需设置类型;保留方法避免编译错误
|
||||||
suctionAspect = aspect;
|
|
||||||
setChanged();
|
|
||||||
markForUpdate();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getStoredEssentia() { return storedEssentia; }
|
public int getStoredEssentia() { return storedEssentia; }
|
||||||
@@ -338,7 +351,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
storedEnergy = tag.getDouble("StoredEnergy");
|
storedEnergy = tag.getDouble("StoredEnergy");
|
||||||
if (storedEnergy > MAX_ENERGY_STORAGE) storedEnergy = MAX_ENERGY_STORAGE;
|
if (storedEnergy > MAX_ENERGY_STORAGE) storedEnergy = MAX_ENERGY_STORAGE;
|
||||||
if (tag.contains("AspectTag")) currentAspect = Aspect.getAspect(tag.getString("AspectTag"));
|
if (tag.contains("AspectTag")) currentAspect = Aspect.getAspect(tag.getString("AspectTag"));
|
||||||
if (tag.contains("SuctionTag")) suctionAspect = Aspect.getAspect(tag.getString("SuctionTag"));
|
// suctionAspect 固定为 null(通配吸力),不读取 SuctionTag
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -352,6 +365,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl
|
|||||||
tag.putInt("StoredEssentia", storedEssentia);
|
tag.putInt("StoredEssentia", storedEssentia);
|
||||||
tag.putDouble("StoredEnergy", storedEnergy);
|
tag.putDouble("StoredEnergy", storedEnergy);
|
||||||
if (currentAspect != null) tag.putString("AspectTag", currentAspect.getTag());
|
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;
|
package thaumicenergistics.init;
|
||||||
|
|
||||||
|
import appeng.api.implementations.menuobjects.IPortableTerminal;
|
||||||
import appeng.api.storage.ITerminalHost;
|
import appeng.api.storage.ITerminalHost;
|
||||||
import appeng.menu.implementations.MenuTypeBuilder;
|
import appeng.menu.implementations.MenuTypeBuilder;
|
||||||
import net.minecraft.core.registries.Registries;
|
import net.minecraft.core.registries.Registries;
|
||||||
@@ -27,8 +28,16 @@ public final class ModMenuTypes {
|
|||||||
REGISTRY.register("essentia_vibration_chamber", () -> IMenuTypeExtension.create(ContainerEssentiaVibrationChamber::new));
|
REGISTRY.register("essentia_vibration_chamber", () -> IMenuTypeExtension.create(ContainerEssentiaVibrationChamber::new));
|
||||||
public static final Supplier<MenuType<ContainerArcaneAssembler>> ARCANE_ASSEMBLER =
|
public static final Supplier<MenuType<ContainerArcaneAssembler>> ARCANE_ASSEMBLER =
|
||||||
REGISTRY.register("arcane_assembler", () -> IMenuTypeExtension.create(ContainerArcaneAssembler::new));
|
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 =
|
public static final Supplier<MenuType<ContainerKnowledgeInscriber>> KNOWLEDGE_INSCRIBER =
|
||||||
REGISTRY.register("knowledge_inscriber", () -> IMenuTypeExtension.create(ContainerKnowledgeInscriber::new));
|
REGISTRY.register("knowledge_inscriber", () -> IMenuTypeExtension.create(ContainerKnowledgeInscriber::new));
|
||||||
public static final Supplier<MenuType<ContainerDistillationPatternEncoder>> DISTILLATION_ENCODER =
|
public static final Supplier<MenuType<ContainerDistillationPatternEncoder>> DISTILLATION_ENCODER =
|
||||||
@@ -38,28 +47,28 @@ public final class ModMenuTypes {
|
|||||||
REGISTRY.register("essentia_import_bus", () ->
|
REGISTRY.register("essentia_import_bus", () ->
|
||||||
MenuTypeBuilder.create(ContainerEssentiaImportBus::new, EssentiaImportBusPart.class)
|
MenuTypeBuilder.create(ContainerEssentiaImportBus::new, EssentiaImportBusPart.class)
|
||||||
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
|
.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 =
|
public static final Supplier<MenuType<ContainerEssentiaExportBus>> ESSENTIA_EXPORT_BUS =
|
||||||
REGISTRY.register("essentia_export_bus", () ->
|
REGISTRY.register("essentia_export_bus", () ->
|
||||||
MenuTypeBuilder.create(ContainerEssentiaExportBus::new, EssentiaExportBusPart.class)
|
MenuTypeBuilder.create(ContainerEssentiaExportBus::new, EssentiaExportBusPart.class)
|
||||||
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
|
.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 =
|
public static final Supplier<MenuType<ContainerEssentiaStorageBus>> ESSENTIA_STORAGE_BUS =
|
||||||
REGISTRY.register("essentia_storage_bus", () ->
|
REGISTRY.register("essentia_storage_bus", () ->
|
||||||
MenuTypeBuilder.create(ContainerEssentiaStorageBus::new, EssentiaStorageBusPart.class)
|
MenuTypeBuilder.create(ContainerEssentiaStorageBus::new, EssentiaStorageBusPart.class)
|
||||||
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
|
.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 =
|
public static final Supplier<MenuType<ContainerArcaneCraftingTerminal>> ARCANE_CRAFTING_TERMINAL =
|
||||||
REGISTRY.register("arcane_crafting_terminal", () ->
|
REGISTRY.register("arcane_crafting_terminal", () ->
|
||||||
MenuTypeBuilder.create(ContainerArcaneCraftingTerminal::new, ITerminalHost.class)
|
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 =
|
public static final Supplier<MenuType<ContainerEssentiaLevelEmitter>> ESSENTIA_LEVEL_EMITTER =
|
||||||
REGISTRY.register("essentia_level_emitter", () ->
|
REGISTRY.register("essentia_level_emitter", () ->
|
||||||
MenuTypeBuilder.create(ContainerEssentiaLevelEmitter::new, EssentiaLevelEmitterPart.class)
|
MenuTypeBuilder.create(ContainerEssentiaLevelEmitter::new, EssentiaLevelEmitterPart.class)
|
||||||
.withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty())
|
.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("tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverters");
|
||||||
Class.forName("thaumicenergistics.integration.jei.Ae2JeiCompat")
|
Class.forName("thaumicenergistics.integration.jei.Ae2JeiCompat")
|
||||||
.getMethod("register").invoke(null);
|
.getMethod("register").invoke(null);
|
||||||
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration detected, essentia drag-conversion registered");
|
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration not present, essentia drag-conversion unavailable (optional)");
|
// AE2-JEI-Integration 未安装:跳过源质拖拽转换(可选功能,不影响其他 JEI 功能)
|
||||||
} catch (ReflectiveOperationException e) {
|
} catch (ReflectiveOperationException e) {
|
||||||
ThaumicEnergistics.LOG.warn("[JEI] AE2-JEI-Integration registration failed: {}", e.toString());
|
ThaumicEnergistics.LOG.warn("[JEI] AE2-JEI-Integration registration failed: {}", e.toString());
|
||||||
}
|
}
|
||||||
@@ -83,7 +82,6 @@ public class ThEJeiPlugin implements IModPlugin {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) {
|
public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) {
|
||||||
ThaumicEnergistics.LOG.info("[JEI] Registering ThEUseCraftingRecipeTransfer for ArcaneCraftingTerminal");
|
|
||||||
// 注册 IRecipeTransferInfo 让 JEI 显示 "+" 按钮
|
// 注册 IRecipeTransferInfo 让 JEI 显示 "+" 按钮
|
||||||
registration.addRecipeTransferHandler(new ArcaneCraftingTransferInfo());
|
registration.addRecipeTransferHandler(new ArcaneCraftingTransferInfo());
|
||||||
registration.addRecipeTransferHandler(new ArcaneRecipeTransferInfo());
|
registration.addRecipeTransferHandler(new ArcaneRecipeTransferInfo());
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package thaumicenergistics.integration.jei;
|
package thaumicenergistics.integration.jei;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -24,10 +26,14 @@ import net.neoforged.neoforge.network.PacketDistributor;
|
|||||||
|
|
||||||
import thaumcraft.integration.jei.JeiArcaneRecipe;
|
import thaumcraft.integration.jei.JeiArcaneRecipe;
|
||||||
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
|
import thaumcraft.integration.jei.ThaumcraftJeiPlugin;
|
||||||
import thaumicenergistics.ThaumicEnergistics;
|
|
||||||
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
|
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
|
||||||
import thaumicenergistics.init.ModMenuTypes;
|
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 奥术配方转移处理器。
|
* 奥术合成终端专用的 JEI 奥术配方转移处理器。
|
||||||
* 点击 "+" 时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。
|
* 点击 "+" 时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。
|
||||||
@@ -65,13 +71,31 @@ public class ThEUseArcaneRecipeTransfer
|
|||||||
boolean maxTransfer,
|
boolean maxTransfer,
|
||||||
boolean doTransfer) {
|
boolean doTransfer) {
|
||||||
|
|
||||||
ThaumicEnergistics.LOG.info("[JEI-Arcane] transferRecipe called, recipeId={}, doTransfer={}",
|
|
||||||
recipe.id(), doTransfer);
|
|
||||||
|
|
||||||
boolean craftMissing = AbstractContainerScreen.hasControlDown();
|
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 (!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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,10 +112,23 @@ public class ThEUseArcaneRecipeTransfer
|
|||||||
// 奥术配方不在 vanilla 配方管理器中,recipeId 传 null
|
// 奥术配方不在 vanilla 配方管理器中,recipeId 传 null
|
||||||
// 服务端会用 Ingredient.of(templateItems) 重建,然后从 ME 网络提取
|
// 服务端会用 Ingredient.of(templateItems) 重建,然后从 ME 网络提取
|
||||||
var packet = new FillCraftingGridFromRecipePacket(null, templateItems, craftMissing);
|
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);
|
PacketDistributor.sendToServer(packet);
|
||||||
|
|
||||||
return null;
|
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) {
|
boolean doTransfer) {
|
||||||
|
|
||||||
var recipe = recipeHolder.value();
|
var recipe = recipeHolder.value();
|
||||||
ThaumicEnergistics.LOG.info("[JEI] transferRecipe called, recipeId={}, doTransfer={}, player={}",
|
|
||||||
recipeHolder.id(), doTransfer, player.getName().getString());
|
|
||||||
if (recipe.getType() != RecipeType.CRAFTING) {
|
if (recipe.getType() != RecipeType.CRAFTING) {
|
||||||
ThaumicEnergistics.LOG.warn("[JEI] Not a crafting recipe, type={}", recipe.getType());
|
ThaumicEnergistics.LOG.warn("[JEI] Not a crafting recipe, type={}", recipe.getType());
|
||||||
return helper.createInternalError();
|
return helper.createInternalError();
|
||||||
@@ -96,16 +94,14 @@ public class ThEUseCraftingRecipeTransfer
|
|||||||
}
|
}
|
||||||
|
|
||||||
boolean craftMissing = AbstractContainerScreen.hasControlDown();
|
boolean craftMissing = AbstractContainerScreen.hasControlDown();
|
||||||
// 自行构建 slot→ingredient 映射(0-8 对应 3x3 合成格),
|
// 用 JEI 官方映射 helper.getGuiSlotIndexToIngredientMap:配方居中摆入 3x3 网格,
|
||||||
// 不依赖 helper.getGuiSlotIndexToIngredientMap(需要 IRecipeTransferInfo 才能正确工作)
|
// 键空间与 JEI 输入槽视图完全一致。之前用 ensure3by3CraftingMatrix(左上对齐填充),
|
||||||
var slotToIngredientMap = buildSlotToIngredientMap(recipe);
|
// 对非 3x3 配方(如 2x2 笔与墨)索引错位,导致缺失高亮画到错误的格子上。
|
||||||
|
var slotToIngredientMap = helper.getGuiSlotIndexToIngredientMap(recipeHolder);
|
||||||
var missingSlots = menu.findMissingIngredients(slotToIngredientMap);
|
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()) {
|
if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) {
|
||||||
ThaumicEnergistics.LOG.info("[JEI] All items missing, returning NO_ITEMS error");
|
|
||||||
var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT);
|
var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT);
|
||||||
var missingSlotViews = missingSlots.missingSlots().stream()
|
var missingSlotViews = missingSlots.missingSlots().stream()
|
||||||
.map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null)
|
.map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null)
|
||||||
@@ -119,7 +115,8 @@ public class ThEUseCraftingRecipeTransfer
|
|||||||
if (!doTransfer) {
|
if (!doTransfer) {
|
||||||
if (missingSlots.totalSize() != 0) {
|
if (missingSlots.totalSize() != 0) {
|
||||||
int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR;
|
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 = 绿色按钮
|
// 全部物品都有 → null = 绿色按钮
|
||||||
return null;
|
return null;
|
||||||
@@ -145,42 +142,28 @@ public class ThEUseCraftingRecipeTransfer
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packet = new FillCraftingGridFromRecipePacket(recipeId, templateItems, craftMissing);
|
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);
|
PacketDistributor.sendToServer(packet);
|
||||||
|
|
||||||
return null;
|
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。
|
* 参考 AE2-JEI-Integration.UseCraftingRecipeTransfer.ErrorRenderer。
|
||||||
|
* 供奥术配方转移(ThEUseArcaneRecipeTransfer)复用。
|
||||||
|
* blockTransfer=true 时返回 ERROR(阻止转移,按钮灰/橙不可点)。
|
||||||
*/
|
*/
|
||||||
private record MissingSlotsError(
|
public record MissingSlotsError(
|
||||||
ContainerArcaneCraftingTerminal.MissingIngredientSlots indices,
|
ContainerArcaneCraftingTerminal.MissingIngredientSlots indices,
|
||||||
boolean craftMissing,
|
boolean craftMissing,
|
||||||
int color) implements IRecipeTransferError {
|
int color,
|
||||||
|
boolean blockTransfer,
|
||||||
|
ContainerArcaneCraftingTerminal menu) implements IRecipeTransferError {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
return Type.COSMETIC;
|
// USER_FACING 阻止转移(allowsTransfer=false)且显示错误;COSMETIC 仅预览
|
||||||
|
return blockTransfer ? Type.USER_FACING : Type.COSMETIC;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -195,16 +178,20 @@ public class ThEUseCraftingRecipeTransfer
|
|||||||
poseStack.pushPose();
|
poseStack.pushPose();
|
||||||
poseStack.translate(recipeX, recipeY, 0);
|
poseStack.translate(recipeX, recipeY, 0);
|
||||||
|
|
||||||
var slotViews = slots.getSlotViews(RecipeIngredientRole.INPUT);
|
// 用 JEI 槽视图 drawHighlight 画高亮(AE2-JEI-Integration 官方做法)。
|
||||||
for (int i = 0; i < slotViews.size(); i++) {
|
// 不能用 menu.slots 的容器坐标:奥术终端槽位顺序与配方格不对应,
|
||||||
var slotView = slotViews.get(i);
|
// 且 translate(recipeX,recipeY) 后再用相对坐标会双重偏移 → 红框画到屏幕外。
|
||||||
|
var inputSlots = slots.getSlotViews(RecipeIngredientRole.INPUT);
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
boolean missing = indices.missingSlots().contains(i);
|
boolean missing = indices.missingSlots().contains(i);
|
||||||
boolean craftable = indices.craftableSlots().contains(i);
|
boolean craftable = indices.craftableSlots().contains(i);
|
||||||
if (missing || craftable) {
|
if (!missing && !craftable) continue;
|
||||||
slotView.drawHighlight(
|
if (i >= inputSlots.size()) continue;
|
||||||
guiGraphics,
|
var slotView = inputSlots.get(i);
|
||||||
|
slotView.drawHighlight(guiGraphics,
|
||||||
missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR);
|
missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR);
|
||||||
}
|
ThaumicEnergistics.LOG.info("[JEI-Transfer] showError slot={} missing={} craftable={}",
|
||||||
|
i, missing, craftable);
|
||||||
}
|
}
|
||||||
|
|
||||||
poseStack.popPose();
|
poseStack.popPose();
|
||||||
@@ -221,15 +208,15 @@ public class ThEUseCraftingRecipeTransfer
|
|||||||
int craftable = indices.craftableSlots().size();
|
int craftable = indices.craftableSlots().size();
|
||||||
if (missing > 0) {
|
if (missing > 0) {
|
||||||
tooltip.add(net.minecraft.network.chat.Component.translatable(
|
tooltip.add(net.minecraft.network.chat.Component.translatable(
|
||||||
"jei.tooltip.transfer.missing", missing));
|
"thaumicenergistics.jei.transfer.missing", missing));
|
||||||
}
|
}
|
||||||
if (craftable > 0) {
|
if (craftable > 0) {
|
||||||
tooltip.add(net.minecraft.network.chat.Component.translatable(
|
tooltip.add(net.minecraft.network.chat.Component.translatable(
|
||||||
"jei.tooltip.transfer.craftable", craftable));
|
"thaumicenergistics.jei.transfer.craftable", craftable));
|
||||||
}
|
}
|
||||||
if (craftMissing) {
|
if (craftMissing) {
|
||||||
tooltip.add(net.minecraft.network.chat.Component.translatable(
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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]]
|
[[dependencies.thaumicenergistics]]
|
||||||
modId="thaumcraft"
|
modId="thaumcraft"
|
||||||
type="required"
|
type="required"
|
||||||
versionRange="[0,)"
|
versionRange="[0.2.2.95,)"
|
||||||
ordering="AFTER"
|
ordering="AFTER"
|
||||||
side="BOTH"
|
side="BOTH"
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,16 @@
|
|||||||
},
|
},
|
||||||
"position": {
|
"position": {
|
||||||
"left": 8,
|
"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_stored": "Stored: %s / %s",
|
||||||
"thaumicenergistics.jade.energy_output": "Output Limit: %s AE/tick",
|
"thaumicenergistics.jade.energy_output": "Output Limit: %s AE/tick",
|
||||||
"thaumicenergistics.jade.tank_full": "Tank full, generation stopped",
|
"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_SCEPTRE": "工艺权杖",
|
||||||
"tc.research_name.PSEUDO_COREGATHER": "傀儡核心:聚集",
|
"tc.research_name.PSEUDO_COREGATHER": "傀儡核心:聚集",
|
||||||
|
|
||||||
"thaumicenergistics.jade.suction_type": "吸力: %s",
|
"thaumicenergistics.jade.current_aspect": "燃料: %s",
|
||||||
"thaumicenergistics.jade.essentia_stored": "源质: %d / %d",
|
"thaumicenergistics.jade.essentia_stored": "源质: %d / %d",
|
||||||
"thaumicenergistics.jade.burning": "燃烧 %d%%, %.1f AE/tick",
|
"thaumicenergistics.jade.burning": "燃烧 %d%%, %.1f AE/tick",
|
||||||
"thaumicenergistics.aspect.ignis": "火",
|
"thaumicenergistics.aspect.ignis": "火",
|
||||||
@@ -130,5 +130,8 @@
|
|||||||
"thaumicenergistics.jade.energy_stored": "已存储: %s / %s",
|
"thaumicenergistics.jade.energy_stored": "已存储: %s / %s",
|
||||||
"thaumicenergistics.jade.energy_output": "输出上限: %s AE/tick",
|
"thaumicenergistics.jade.energy_output": "输出上限: %s AE/tick",
|
||||||
"thaumicenergistics.jade.tank_full": "能量槽已满,停止发电",
|
"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 合成缺失物品"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 215 KiB |
@@ -8,11 +8,13 @@
|
|||||||
"ae2.MixinCraftingTreeProcess",
|
"ae2.MixinCraftingTreeProcess",
|
||||||
"ae2.MixinAbstractTerminalPart",
|
"ae2.MixinAbstractTerminalPart",
|
||||||
"ae2.MixinConfigMenuInventory",
|
"ae2.MixinConfigMenuInventory",
|
||||||
|
"ae2.MixinMEStorageMenuAccessors",
|
||||||
"tc.MixinAspect",
|
"tc.MixinAspect",
|
||||||
"tc.MixinVisRelayBlock"
|
"tc.MixinVisRelayBlock"
|
||||||
],
|
],
|
||||||
"client": [
|
"client": [
|
||||||
"ae2.MixinCraftConfirmScreen",
|
"ae2.MixinCraftConfirmScreen",
|
||||||
|
"ae2.MixinMEStorageScreen",
|
||||||
"tc.ResearchPageIndexMixin",
|
"tc.ResearchPageIndexMixin",
|
||||||
"tc.MixinThaumonomiconScreen"
|
"tc.MixinThaumonomiconScreen"
|
||||||
],
|
],
|
||||||
|
|||||||