Compare commits

...
18 Commits
Author SHA1 Message Date
1820982382 b80922e1f1 v2.3.14 2026-09-23 14:50:31 +08:00
1820982382 2e6a8e9590 v2.3.14 2026-09-23 14:49:11 +08:00
1820982382 2ee393b4e6 v2.3.14 2026-09-23 14:47:21 +08:00
1820982382 0efd437209 v2.3.14 2026-09-23 14:39:07 +08:00
1820982382 ad0ead9f53 v2.3.9 2026-08-28 00:43:18 +08:00
1820982382 7a914571d8 v2.3.9 2026-08-28 00:42:45 +08:00
1820982382 14aaf0b1a9 v2.3.9 2026-08-28 00:41:54 +08:00
1820982382 177ef6c551 v2.2.8 2026-08-22 17:07:18 +08:00
1820982382 c83e34c1d6 v2.2.7 2026-08-22 16:59:38 +08:00
1820982382 0aa6db226e [全流程完成] 2026-08-19 00:18:43 +08:00
1820982382 fc72ebca78 [全流程完成] 2026-08-19 00:17:24 +08:00
1820982382 2e9f1fd4e3 [全流程完成] 2026-08-17 15:18:53 +08:00
1820982382 835a37a0c7 [全流程完成] 2026-08-17 15:17:42 +08:00
1820982382 da04b67ef0 [全流程完成] 2026-08-17 15:15:33 +08:00
1820982382 13b981a916 [全流程完成] 2026-08-17 15:15:04 +08:00
1820982382 286fc2d2c3 修改gitignore 2026-08-17 15:14:30 +08:00
1820982382 8673074e60 [修正] 奥术配方研究 key 修复与奥术合成终端充能完善
- 配方: 1k/4k/16k/64k 存储组件、无线傀儡背包、源质发信器
  研究 key 改为实际注册节点 (ESSENTIASTORAGE / GOLEMWIFIBACKPACK / ESSENTIATERMINAL)
- 配方: 扩散/凝聚核心材料 quicksilver → quicksilver_drop
- 配方: 知识记录仪/蒸馏编码台/源质终端 显示器三变体合并为
  thaumicenergistics:monitors tag (9 份重复配方 → 3 份)
- 终端: 新增法杖自动充能 (内存卡链接 Vis Interface P2P 频率,
  IGridTickable 每 tick 抽取, 速率与原版充能中继一致 5 cV/tick)
- 装配器: 删除无调用方且会静默失败的 findMatchingArcaneRecipe;
  vis 折扣魔法值统一为 BASE_VIS_DISCOUNT 常量
- 清理: 删除 Feature 空壳层/ThEFeatureBase/AEPartGridBlock 等死代码
  (-362 行), 删除空 registerScanEvent, 硬编码节点数, lang 死键
- 编译: IDEA 全项目通过 (problems: [])
2026-08-09 06:00:07 +08:00
1820982382 0e85bfc303 [修正] 奥术配方研究 key 修复与奥术合成终端充能完善
- 配方: 1k/4k/16k/64k 存储组件、无线傀儡背包、源质发信器
  研究 key 改为实际注册节点 (ESSENTIASTORAGE / GOLEMWIFIBACKPACK / ESSENTIATERMINAL)
- 配方: 扩散/凝聚核心材料 quicksilver → quicksilver_drop
- 配方: 知识记录仪/蒸馏编码台/源质终端 显示器三变体合并为
  thaumicenergistics:monitors tag (9 份重复配方 → 3 份)
- 终端: 新增法杖自动充能 (内存卡链接 Vis Interface P2P 频率,
  IGridTickable 每 tick 抽取, 速率与原版充能中继一致 5 cV/tick)
- 装配器: 删除无调用方且会静默失败的 findMatchingArcaneRecipe;
  vis 折扣魔法值统一为 BASE_VIS_DISCOUNT 常量
- 清理: 删除 Feature 空壳层/ThEFeatureBase/AEPartGridBlock 等死代码
  (-362 行), 删除空 registerScanEvent, 硬编码节点数, lang 死键
- 编译: IDEA 全项目通过 (problems: [])
2026-08-09 05:58:22 +08:00
133 changed files with 12835 additions and 1010 deletions
+4979
View File
File diff suppressed because it is too large Load Diff
+86
View File
@@ -0,0 +1,86 @@
# [Feature Request] 改善物品/方块扫描要素的自动赋予机制(恢复配方溯源 + 性能优化)
## 概述 Summary
请求为 TC4 1.21.1 移植版恢复 1.7.10 的**合成配方溯源**机制(物品作为配方产出时,按原料要素自动赋予要素),并修复 `getObjectTags` 无索引穷举遍历导致的严重性能问题。
当前移植版 `ItemAspectRegistry.getObjectTags` 只保留:注册表 → NBT → 精确表 → ID 词根推算 → `ENTROPY` 兜底。**词根表仅覆盖约 15 组 ID**,对 mod 物品几乎不命中,大量物品退化为 `ENTROPY×1` 或词根给的 1-2 种要素,且无法反映合成复杂度。
## 问题描述 Problem
### 功能缺失(影响整个 mod 生态)
1. **要素不准确**:mod 物品(如 `thaumicenergistics:essentia_provider`、`waystones:waystone`)扫描要素靠 ID 词根猜——词根对不上就 `ENTROPY×1`,对得上也只有 1-3 种
2. **无合成溯源**:合成材料完全不影响要素——1 个原料的和 10 个原料的合成品要素一样,无法体现合成复杂度
### 性能严重(实测)
- `getObjectTags` 每次调用**穷举遍历全部配方**(1932+ 个 CRAFTING)比较产出——无索引、无缓存
- 客户端 JEI 启动遍历全部物品时触发全量扫描:**实测单次扫描耗时 10 秒 / 1127 tick**(F3+L profiler)
- 频繁调用(Thaumonomicon/JEI 显示)导致持续卡顿
## 期望行为 Expected Behavior
- mod 物品扫描要素**按合成材料赋予**(与 1.7.10 一致)
- 要素数量/种类**合理**(种类 2-6、总量 8-24、单要素 ≤64)
- 扫描/显示**无卡顿**(单次查询毫秒级)
## 建议实现 Suggested Implementation
### A. 功能:getObjectTags 增加配方溯源分支
```
在词根推算 / ENTROPY 兜底之前:
recipe = 查产出=stack 的配方
if recipe != null:
原料要素 = 递归 getObjectTags(原料)(深度 ≤2,visited 防环)
返回 原料要素之和(同种要素取最大,避免累加爆炸)
```
可选覆盖配方类型(按需):普通合成(Crafting)、熔炉(Smelting)、注魔(Infusion)、炼金(Crucible)、奥术(Arcane)。
### B. 性能:产出→配方索引
```
懒构建 Map<Item, List<Recipe>>(按产出分组,首次查询时建一次)
getObjectTags 内 O(1) 查索引,不再每次穷举遍历全部配方
结果缓存 ConcurrentHashMap<Item, AspectList>,绑定 Server 引用(/reload 自动失效)
```
### C. 要素限制(贴近 1.7.10 + 平衡)
- 单要素 ≤64
- 种类 ≤6(按数量降序取前 6)
- 总量按复杂度归一化:`clamp(8 + 原料种类×2, 8, 24)`
- 同种要素多个原料出现时**取最大值**(不累加,防 1000+)
## 复现步骤 Steps to Reproduce
1. 安装任意含合成配方的 mod(如 waystones 传送石碑:3 黑曜石 + 3 石砖 + 1 传送石)
2. 用魔导透镜扫描该 mod 物品
3. 实际:`ENTROPY×1` 或词根 1-3 种(传送石碑给 1 混沌)
4. 期望:黑曜石(暗/虚空)+ 石砖(石头)+ 传送石(水晶/末影)要素
## 性能复现
1. 客户端启动(JEI 加载,遍历全部物品调 getObjectTags)
2. F3+L 采样 10 秒
3. 实际:`getObjectTags` 占比极高(穷举遍历配方),单物品 10s 级
## 参考实现要点(作者可参考)
- 1.21.1 `RecipeManager.byType` 为 **private**——用 `getAllRecipesFor(RecipeType)`(公开,返回 `List<RecipeHolder<T>>`)
- `RecipeHolder.id()` 直接返回 `ResourceLocation`(无 `.location()`)
- TC 配方访问:`ThaumcraftApi.getInfusionRecipe(result)` / `getCrucibleRecipe(result)` / `getCraftingRecipes()`(遍历 `IArcaneRecipe`,含 `getAspects()` vis 成本)
- `InfusionRecipe` 原料:`centralInput()` + `getComponents()`(公开)
- 缓存注意:组件敏感物品(药水/染色/耐久 NBT 变体)不进缓存(TC 的 NBT 分支先截走);COMPUTING 哨兵防跨配方递归环;缓存绑定 Server 引用(reload 失效)
## 环境 Environment
- Minecraft 1.21.1
- NeoForge 21.1.x
- Thaumcraft
## 补充
- 完整反编译调研与参考实现已由 ThaumicEnergistics 移植团队完成,可提供更详细方案(含存储体系:内存缓存 + 可选本地二进制文件缓存,指纹校验跨启动复用)
+29
View File
@@ -0,0 +1,29 @@
This is a community‑port of ThaumicEnergistics.
Modifications and port code: Copyright (c) 2026 AlkSur, Licensed under GNU LGPL‑3.0.
Original source code: Copyright (c) Chris, BrockWS, Licensed under MIT License (see LICENSE‑ORIGINAL‑MIT).
Original License:
The MIT License (MIT)
Copyright (c) 2018-2019 Chris, BrockWS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+40 -10
View File
@@ -21,7 +21,6 @@ repositories {
} }
} }
version = mod_version
group = mod_group group = mod_group
var modName = mod_name var modName = mod_name
@@ -45,9 +44,11 @@ neoForge {
dependencies { dependencies {
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api" compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17:api"
compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17" // 加这行保留!!让AE2内部类可编译 compileOnly "org.appliedenergistics:appliedenergistics2:19.2.17" // 加这行保留!!让AE2内部类可编译
compileOnly files('library/thaumcraft-0.2.2.34-port.1.jar') compileOnly files('library/thaumcraft-0.2.2.95-port.229.jar') // 必须与整合包运行版本一致(0.2.2.95);.34 缺少 getRemainingItems/findMatchingArcaneRecipeEntry 等 API
compileOnly files('library/jei-1.21.1-neoforge-19.39.0.368.jar') compileOnly files('library/jei-1.21.1-neoforge-19.39.0.368.jar')
compileOnly files('library/ae2jeiintegration-1.2.1.jar') compileOnly files('library/ae2jeiintegration-1.2.1.jar')
compileOnly files('library/InventoryProfilesNext-neoforge-1.21.1-2.2.5.jar')
compileOnly files('library/extendedae_plus-1.6.1.jar') // eap mixin target(InputEvents.toGenericStack)
compileOnly "maven.modrinth:jade:${jade_version}" compileOnly "maven.modrinth:jade:${jade_version}"
} }
@@ -57,7 +58,8 @@ tasks.withType(JavaCompile).configureEach {
it.options.release = 21 it.options.release = 21
} }
var modVersion = mod_version // mods.toml 的 version 也用 git 实时版本(与 jar 版本一致,不依赖写死的 mod_version)
var modVersion = gitVersion()
def loaderRange = project.findProperty('loader_version_range') ?: '[4,)' def loaderRange = project.findProperty('loader_version_range') ?: '[4,)'
def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)' def neoforgeRange = project.findProperty('neoforge_version_range') ?: '[21,)'
def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)' def mcRange = project.findProperty('minecraft_version_range') ?: '[1.21.1,1.22)'
@@ -71,12 +73,40 @@ processResources {
} }
} }
jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } jar {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
// PCL/启动器显示的 jar 图标(mod 列表图标走 neoforge.mods.toml 的 logoFile)
from('src/main/resources') {
include 'pack.png'
}
}
// 获取git提交总数
def gitCommitCount = providers.exec { //// 获取git提交总数
//def gitCommitCount = providers.exec {
// commandLine 'git', 'rev-list', '--count', 'HEAD'
//}.standardOutput.asText.map { it.trim() }
//
//// 最终完整版本 = 主版本+构建号
//version = "${mod_version}.${gitCommitCount.get()}-alpha"
// build.gradle 替换现有 version 逻辑
def gitVersion() {
def describe = providers.exec {
commandLine 'git', 'describe', '--tags', '--always'
}.standardOutput.asText.map { it.trim() }.get()
// 支持 "2.2" / "v2.2.0" / "2.2-3-gabc1234" / "abc1234"(无tag时)
// 版本 = tag 的 X.Y + 提交数:tag 2.2 + 2 提交 → 2.2.2
// find() 只匹配开头,忽略尾部 commit hash(-gabc1234)
def m = describe =~ /^v?(\d+\.\d+)(?:\.\d+)?(?:-(\d+))?/
if (m.find()) {
def xy = m.group(1) // 2.2
def count = m.group(2) ? m.group(2) : '0' // tag 后提交数
return "${xy}.${count}-alpha" // 2.2.2-alpha
}
// 兜底(从未打 tag):0.0.<提交总数>-alpha,不依赖写死的 mod_version
def count = providers.exec {
commandLine 'git', 'rev-list', '--count', 'HEAD' commandLine 'git', 'rev-list', '--count', 'HEAD'
}.standardOutput.asText.map { it.trim() } }.standardOutput.asText.map { it.trim() }.get()
return "0.0.${count}-alpha"
// 最终完整版本 = 主版本+构建号 }
version = "${mod_version}.${gitCommitCount.get()}-alpha" version = modVersion
+1 -1
View File
@@ -1,11 +1,11 @@
# Project # Project
mod_version=2.0.1
mod_group=thaumicenergistics mod_group=thaumicenergistics
# Mod # Mod
mod_name=ThaumicEnergistics mod_name=ThaumicEnergistics
mod_id=thaumicenergistics mod_id=thaumicenergistics
mod_author=Nividica & Community mod_author=Nividica & Community
modLicense=LGPL-3.0
# Shared (must match root gradle.properties) # Shared (must match root gradle.properties)
minecraft_version=1.21.1 minecraft_version=1.21.1
Vendored
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -1,6 +1,8 @@
package thaumicenergistics; package thaumicenergistics;
//
//AlkSur
import appeng.api.features.GridLinkables; import appeng.api.features.GridLinkables;
import appeng.api.features.P2PTunnelAttunement;
import appeng.api.parts.PartModels; import appeng.api.parts.PartModels;
import appeng.api.stacks.AEKeyType; import appeng.api.stacks.AEKeyType;
import appeng.api.stacks.AEKeyTypesInternal; import appeng.api.stacks.AEKeyTypesInternal;
@@ -19,7 +21,6 @@ import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import thaumicenergistics.common.container.*; import thaumicenergistics.common.container.*;
import thaumicenergistics.common.features.FeatureRegistry;
import thaumicenergistics.common.features.RecipeRegistration; import thaumicenergistics.common.features.RecipeRegistration;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType; import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.common.integration.appeng.ThEAppliedEnergistics; import thaumicenergistics.common.integration.appeng.ThEAppliedEnergistics;
@@ -63,6 +64,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(
@@ -107,12 +110,11 @@ public final class ThaumicEnergistics {
event.enqueueWork(() -> { event.enqueueWork(() -> {
RecipeRegistration.registerAll(); RecipeRegistration.registerAll();
ThEThaumcraft.init(); ThEThaumcraft.init();
P2PTunnelAttunement.registerAttunementTag(ModItems.VIS_INTERFACE.get());
GridLinkables.register(ModItems.GOLEM_WIFI_BACKPACK.get(), ItemGolemWirelessBackpack.LINKABLE_HANDLER); GridLinkables.register(ModItems.GOLEM_WIFI_BACKPACK.get(), ItemGolemWirelessBackpack.LINKABLE_HANDLER);
GridLinkables.register(ModItems.WIRELESS_ESSENTIA_TERMINAL.get(), GridLinkables.register(ModItems.WIRELESS_ESSENTIA_TERMINAL.get(),
appeng.items.tools.powered.WirelessTerminalItem.LINKABLE_HANDLER); appeng.items.tools.powered.WirelessTerminalItem.LINKABLE_HANDLER);
}); });
FeatureRegistry.registerAll();
} }
private void registerUpgrades(FMLCommonSetupEvent event) { private void registerUpgrades(FMLCommonSetupEvent event) {
@@ -148,7 +150,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);
@@ -206,5 +215,10 @@ public final class ThaumicEnergistics {
AECapabilities.IN_WORLD_GRID_NODE_HOST, AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.ARCANE_ASSEMBLER.get(), ModBlockEntities.ARCANE_ASSEMBLER.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be); (be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
// 注魔监视器:连接 AE 网络(network 状态)需要网格节点 capability
event.registerBlockEntity(
AECapabilities.IN_WORLD_GRID_NODE_HOST,
ModBlockEntities.INFUSION_MONITOR.get(),
(be, context) -> (appeng.api.networking.IInWorldGridNodeHost) be);
} }
} }
@@ -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));
}
}
@@ -1,5 +1,7 @@
package thaumicenergistics.common.container; package thaumicenergistics.common.container;
import java.util.List;
import appeng.core.definitions.AEItems; import appeng.core.definitions.AEItems;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
@@ -13,7 +15,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;
@@ -33,28 +35,28 @@ import thaumicenergistics.init.ModMenuTypes;
*/ */
public class ContainerArcaneAssembler extends AbstractContainerMenu { public class ContainerArcaneAssembler extends AbstractContainerMenu {
// ===== 坐标常量(1:1 对应1.7.10) ===== // ===== 坐标常量(新版贴图 256×256:格子 16×16、步距 18) =====
private static final int PLAYER_INV_X = 8; private static final int PLAYER_INV_X = 8;
private static final int PLAYER_INV_Y = 113; private static final int PLAYER_INV_Y = 147;
private static final int HOTBAR_Y = PLAYER_INV_Y + 58; // 173 private static final int HOTBAR_Y = 205;
private static final int PATTERN_X = 26; private static final int PATTERN_X = 26;
private static final int PATTERN_Y = 25; private static final int PATTERN_Y = 15;
private static final int PATTERN_ROWS = 3; private static final int PATTERN_ROWS = 3;
private static final int PATTERN_COLS = 7; private static final int PATTERN_COLS = 7;
private static final int KCORE_X = 187; private static final int KCORE_X = 175;
private static final int KCORE_Y = 8; private static final int KCORE_Y = 6;
private static final int UPGRADE_X = 187; private static final int UPGRADE_X = 175;
private static final int UPGRADE_Y = 26; private static final int UPGRADE_Y = 24;
private static final int UPGRADE_COUNT = 4; private static final int UPGRADE_COUNT = 4;
private static final int TARGET_X = 14; private static final int TARGET_X = 114;
private static final int TARGET_Y = 87; private static final int TARGET_Y = 97;
private static final int ARMOR_X = 210; private static final int ARMOR_X = 152;
private static final int ARMOR_Y = 26; private static final int ARMOR_Y = 73;
private static final int ARMOR_COUNT = 4; private static final int ARMOR_COUNT = 4;
// ===== 槽位划分(quickMoveStack 用) ===== // ===== 槽位划分(quickMoveStack 用) =====
@@ -69,21 +71,29 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
private static final int ARMOR_START = TARGET_IDX + 1; // 63 private static final int ARMOR_START = TARGET_IDX + 1; // 63
private static final int ARMOR_END = ARMOR_START + ARMOR_COUNT; // 67 private static final int ARMOR_END = ARMOR_START + ARMOR_COUNT; // 67
/** 升级卡 handler(独立4格,与 Tile 的 speedUpgrades 字段同步) */
final ItemStackHandler upgradeHandler = new ItemStackHandler(UPGRADE_COUNT) {
@Override protected void onContentsChanged(int slot) {
super.onContentsChanged(slot);
if (assembler != null) {
int count = 0;
for (int i = 0; i < UPGRADE_COUNT; i++)
if (!getStackInSlot(i).isEmpty()) count++;
assembler.setSpeedUpgrades(count);
}
}
};
public final TileArcaneAssembler assembler; public final TileArcaneAssembler assembler;
/** 供 GUI 渲染空槽图标(加速卡/护甲)时定位槽位。 */
public Slot getUpgradeSlot(int i) { return slots.get(UPGRADE_START + i); }
public Slot getArmorSlot(int i) { return slots.get(ARMOR_START + i); }
public Slot getKCoreSlot() { return slots.get(KCORE_IDX); }
// ===== 配方预览(客户端,由 AssemblerPreviewSyncPacket 更新) =====
public boolean clientPreviewActive = false;
public ItemStack[] clientPreviewGrid = new ItemStack[9];
public ItemStack clientPreviewResult = ItemStack.EMPTY;
public void applyClientPreview(List<ItemStack> grid, ItemStack result, boolean active) {
this.clientPreviewActive = active;
this.clientPreviewGrid = new ItemStack[9];
if (grid != null) {
for (int i = 0; i < 9 && i < grid.size(); i++) {
this.clientPreviewGrid[i] = grid.get(i) != null ? grid.get(i).copy() : ItemStack.EMPTY;
}
}
this.clientPreviewResult = result != null ? result.copy() : ItemStack.EMPTY;
}
/** 可视数据同步:服务端读 tile,客户端存同步值(GUI 进度条/源质条不依赖 BE 网络同步,靠容器每 tick 同步) */ /** 可视数据同步:服务端读 tile,客户端存同步值(GUI 进度条/源质条不依赖 BE 网络同步,靠容器每 tick 同步) */
public final ContainerData visData; public final ContainerData visData;
@@ -160,21 +170,18 @@ 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速度卡(持久化在 Tile 槽 27~30,关 GUI 不丢失) =====
for (int i = 0; i < UPGRADE_COUNT; i++) { for (int i = 0; i < UPGRADE_COUNT; i++) {
addSlot(new SlotItemHandler(upgradeHandler, i, UPGRADE_X, UPGRADE_Y + i * 18) { int invIdx = TileArcaneAssembler.UPGRADE_SLOT_START + i;
addSlot(new SlotItemHandler(machineHandler, invIdx, UPGRADE_X, UPGRADE_Y + i * 18) {
@Override @Override
public boolean mayPlace(ItemStack s) { public boolean mayPlace(ItemStack s) {
return AEItems.SPEED_CARD.is(s); return AEItems.SPEED_CARD.is(s);
@@ -182,11 +189,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
}); });
} }
// ===== 6. 目标产物输出槽(只读展示) ===== // ===== 6. 目标产物输出槽(只读展示,由 GUI 从网络包数据渲染;槽位移出屏幕避免双渲染) =====
addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y) { addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, -2000, -2000));
@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++) {
@@ -198,13 +202,25 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu {
case 2 -> net.minecraft.world.entity.EquipmentSlot.LEGS; case 2 -> net.minecraft.world.entity.EquipmentSlot.LEGS;
default -> net.minecraft.world.entity.EquipmentSlot.FEET; default -> net.minecraft.world.entity.EquipmentSlot.FEET;
}; };
// 空槽图标:原版头盔/胸甲/护腿/靴子(BLOCK_ATLAS 图集,同 WCWT 手动合成区做法)
final net.minecraft.resources.ResourceLocation armorIcon = switch (i) {
case 0 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_HELMET;
case 1 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_CHESTPLATE;
case 2 -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_LEGGINGS;
default -> net.minecraft.world.inventory.InventoryMenu.EMPTY_ARMOR_SLOT_BOOTS;
};
addSlot(new SlotItemHandler(machineHandler, invIdx, ARMOR_X, ARMOR_Y + i * 18) { addSlot(new SlotItemHandler(machineHandler, invIdx, ARMOR_X, ARMOR_Y + i * 18) {
@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 com.mojang.datafixers.util.Pair<net.minecraft.resources.ResourceLocation, net.minecraft.resources.ResourceLocation> getNoItemIcon() {
return com.mojang.datafixers.util.Pair.of(
net.minecraft.world.inventory.InventoryMenu.BLOCK_ATLAS, armorIcon);
} }
@Override public void setChanged() { @Override public void setChanged() {
super.setChanged(); super.setChanged();
@@ -220,13 +236,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() {
@@ -1,6 +1,7 @@
package thaumicenergistics.common.container; package thaumicenergistics.common.container;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@@ -22,6 +23,7 @@ import appeng.api.inventories.InternalInventory;
import appeng.api.networking.energy.IEnergySource; import appeng.api.networking.energy.IEnergySource;
import appeng.api.stacks.AEItemKey; import appeng.api.stacks.AEItemKey;
import appeng.api.storage.ITerminalHost; import appeng.api.storage.ITerminalHost;
import appeng.api.storage.StorageHelper;
import appeng.helpers.ICraftingGridMenu; import appeng.helpers.ICraftingGridMenu;
import appeng.menu.SlotSemantics; import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync; import appeng.menu.guisync.GuiSync;
@@ -30,13 +32,17 @@ import appeng.menu.me.crafting.CraftConfirmMenu;
import appeng.menu.slot.AppEngSlot; import appeng.menu.slot.AppEngSlot;
import appeng.menu.slot.CraftingMatrixSlot; import appeng.menu.slot.CraftingMatrixSlot;
import appeng.me.storage.LinkStatusRespectingInventory; import appeng.me.storage.LinkStatusRespectingInventory;
import appeng.util.inv.PlayerInternalInventory;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.ArcaneTerminalClearC2SPacket;
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 thaumicenergistics.ThaumicEnergistics; import thaumcraft.common.vis.TCVisHelper;
import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot; import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot;
import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart; import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
@@ -55,6 +61,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 +110,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 +122,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 类型的物品放入
@@ -104,15 +137,15 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
}, SlotSemantics.STORAGE); }, SlotSemantics.STORAGE);
updateCurrentRecipeAndOutput(true); updateCurrentRecipeAndOutput(true);
registerClientAction(ACTION_CLEAR_TO_PLAYER, this::clearToPlayerInventory);
} }
// ========== ICraftingGridMenu 实现 ========== // ========== ICraftingGridMenu 实现 ==========
@Override @Override
public InternalInventory getCraftingMatrix() { public InternalInventory getCraftingMatrix() {
var inv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING); return this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
ThaumicEnergistics.LOG.debug("[ICraftingGrid] getCraftingMatrix called, size={}", inv.size());
return inv;
} }
@Override @Override
@@ -125,54 +158,162 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return this.energySource; return this.energySource;
} }
/**
* 清空合成网格,材料一键存入 ME 网络(对应终端 S_STASH 按钮与"关闭时自动清理合成槽"设置)。
* 仅客户端调用:发送专用 C2S 包,服务端直接执行 {@link #clearGridToNetworkOnServer()}
* (不走 AE2 quickMove/MOVE_REGION 语义链,任何环境下都可靠)。
*/
public void clearCraftingGrid() {
if (!isClientSide()) {
return;
}
PacketDistributor.sendToServer(
new ArcaneTerminalClearC2SPacket(this.containerId, ArcaneTerminalClearC2SPacket.ACTION_STASH, false));
}
/** 服务端:把合成网格所有物品存入 ME 网络,放不下的回玩家背包。结果槽由 slotsChanged 联动刷新。 */
public void clearGridToNetworkOnServer() {
var grid = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
var playerInv = new PlayerInternalInventory(getPlayerInventory());
boolean changed = false;
for (int i = 0; i < grid.size(); i++) {
ItemStack stack = grid.getStackInSlot(i);
if (stack.isEmpty()) continue;
changed = true;
long inserted = 0;
AEItemKey key = AEItemKey.of(stack);
if (key != null && canInteractWithGrid()) {
inserted = StorageHelper.poweredInsert(energySource, storage, key, stack.getCount(),
this.getActionSource());
}
if (inserted >= stack.getCount()) {
grid.setItemDirect(i, ItemStack.EMPTY);
} else {
ItemStack remaining = stack.copy();
remaining.shrink((int) inserted);
ItemStack left = playerInv.addItems(remaining);
grid.setItemDirect(i, left.isEmpty() ? ItemStack.EMPTY : left);
}
}
if (changed) {
slotsChanged(grid.toContainer());
}
}
/** 服务端:客户端上报的"关闭时自动清理合成槽"开关值(在菜单构造后由 C2S 包设置)。 */
private boolean clearOnClose = false;
public void setClearOnClose(boolean clearOnClose) {
this.clearOnClose = clearOnClose;
}
/**
* 菜单关闭时(无论 ESC、走开、方块被拆、服务端强制关闭),按客户端上报的开关清理网格。
* 不依赖 GUI onClose 是否触发——这是最可靠的兜底。
*/
@Override
public void removed(net.minecraft.world.entity.player.Player player) {
if (!isClientSide() && clearOnClose) {
clearGridToNetworkOnServer();
}
super.removed(player);
}
private static final String ACTION_CLEAR_TO_PLAYER = "clearToPlayer";
/**
* 清空合成网格,材料一键存入玩家背包(对应终端 S_STASH_TO_PLAYER_INV 按钮)。
* 复刻 {@code appeng.menu.me.items.CraftingTermMenu.clearToPlayerInventory}。
*/
public void clearToPlayerInventory() {
if (isClientSide()) {
sendClientAction(ACTION_CLEAR_TO_PLAYER);
return;
}
var craftingGridInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING);
var playerInv = new PlayerInternalInventory(getPlayerInventory());
for (int i = 0; i < craftingGridInv.size(); ++i) {
for (int emptyLoop = 0; emptyLoop < 2; ++emptyLoop) {
boolean allowEmpty = emptyLoop == 1;
// 快捷栏优先
final int HOTBAR_SIZE = 9;
for (int j = HOTBAR_SIZE; j-- > 0;) {
if (playerInv.getStackInSlot(j).isEmpty() == allowEmpty) {
craftingGridInv.setItemDirect(i,
playerInv.getSlotInv(j).addItems(craftingGridInv.getStackInSlot(i)));
}
}
// 其余背包
for (int j = HOTBAR_SIZE; j < Inventory.INVENTORY_SIZE; ++j) {
if (playerInv.getStackInSlot(j).isEmpty() == allowEmpty) {
craftingGridInv.setItemDirect(i,
playerInv.getSlotInv(j).addItems(craftingGridInv.getStackInSlot(i)));
}
}
}
}
}
// ========== 配方检测 ========== // ========== 配方检测 ==========
@Override @Override
public void slotsChanged(Container inventory) { public void slotsChanged(Container inventory) {
ThaumicEnergistics.LOG.debug("[A] slotsChanged fired, beingTaken={}, inventoryClass={}",
isCraftingResultBeingTaken, inventory.getClass().getSimpleName());
if (!isCraftingResultBeingTaken) { if (!isCraftingResultBeingTaken) {
updateCurrentRecipeAndOutput(false); updateCurrentRecipeAndOutput(false);
} else {
ThaumicEnergistics.LOG.debug("[A] slotsChanged SKIPPED (beingTaken=true)");
} }
} }
public void setCraftingResultBeingTaken(boolean beingTaken) { public void setCraftingResultBeingTaken(boolean beingTaken) {
ThaumicEnergistics.LOG.debug("[A] setCraftingResultBeingTaken={}", beingTaken);
this.isCraftingResultBeingTaken = beingTaken; this.isCraftingResultBeingTaken = beingTaken;
} }
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);
if (cost == null || cost.isEmpty()) { if (cost == null || cost.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] no cost, no wand needed");
return true; return true;
} }
var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND); var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND);
var wandStack = wandInv.getStackInSlot(0); var wandStack = wandInv.getStackInSlot(0);
ThaumicEnergistics.LOG.debug("[V] wandStack={}", wandStack);
if (wandStack.isEmpty()) { if (wandStack.isEmpty()) {
ThaumicEnergistics.LOG.debug("[V] wandStack EMPTY");
return false; return false;
} }
if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) { if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
ThaumicEnergistics.LOG.debug("[V] not a WandCastingItem");
return false; return false;
} }
var centivisCost = new AspectList(); var centivisCost = new AspectList();
for (var aspect : cost.aspects()) { for (var aspect : cost.aspects()) {
centivisCost.add(aspect, cost.amount(aspect) * CENTIVIS); centivisCost.add(aspect, cost.amount(aspect) * CENTIVIS);
} }
boolean result = wand.consumeVisCost(wandStack, getPlayerInventory().player, centivisCost, false, true); return wand.consumeVisCost(wandStack, getPlayerInventory().player, centivisCost, false, true);
ThaumicEnergistics.LOG.debug("[V] consumeVisCost result={}", result);
return result;
} }
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 +324,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 +346,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 +357,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 +365,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)");
} }
} }
@@ -332,6 +457,98 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC
return new MissingIngredientSlots(missingSlots, craftableSlots); return new MissingIngredientSlots(missingSlots, craftableSlots);
} }
/**
* 为 JEI "+" 填充挑选一个可用的材料模板:优先取 ME 网络中实际有货的变体,
* 其次玩家背包有货的变体,再次可自动合成的变体,最后回退到第一个变体。
* <p>
* 背景:奥术配方 recipeId=null,服务端只能用客户端模板重建单物品 Ingredient,
* 若客户端固定发第一个变体(如石头),网络里只有深板岩/圆石时槽位就拉不到东西。
* 这里带预留计数(reservedAmounts),多个槽位需要同一材料时不会重复占用同一份库存。
*
* @param stored 网络库存快照(AEItemKey → 数量),由调用方一次构建,避免每槽全量扫描
* @param craftable 网络中可自动合成的物品集合快照
*/
public ItemStack pickAvailableTemplate(Ingredient ingredient, Map<AEItemKey, Long> stored,
Set<AEItemKey> craftable, Object2IntOpenHashMap<AEItemKey> reservedAmounts) {
ItemStack[] items = ingredient.getItems();
if (items.length == 0) {
return ItemStack.EMPTY;
}
// 1) ME 网络中有库存的变体
for (ItemStack variant : items) {
AEItemKey key = AEItemKey.of(variant);
if (key == null) continue;
long avail = stored.getOrDefault(key, 0L) - reservedAmounts.getOrDefault(key, 0);
if (avail >= 1) {
reservedAmounts.merge(key, 1, Integer::sum);
return variant.copyWithCount(1);
}
}
// 2) 玩家背包中的变体
var playerItems = getPlayerInventory().items;
for (int i = 0; i < playerItems.size(); i++) {
if (isPlayerInventorySlotLocked(i)) continue;
var stack = playerItems.get(i);
if (stack.isEmpty() || !ingredient.test(stack)) continue;
AEItemKey key = AEItemKey.of(stack);
int used = key != null ? reservedAmounts.getOrDefault(key, 0) : 0;
if (stack.getCount() - used >= 1) {
if (key != null) {
reservedAmounts.merge(key, 1, Integer::sum);
}
return stack.copyWithCount(1);
}
}
// 3) 可自动合成的变体(仅当玩家按住 Ctrl 时服务端才会真正排产;不按则槽位保持空,与旧行为一致)
for (ItemStack variant : items) {
AEItemKey key = AEItemKey.of(variant);
if (key != null && craftable.contains(key) && reservedAmounts.getOrDefault(key, 0) == 0) {
reservedAmounts.merge(key, 1, Integer::sum);
return variant.copyWithCount(1);
}
}
// 4) 回退:第一个变体
return items[0].copyWithCount(1);
}
/**
* 构建 ME 网络库存快照(AEItemKey → 存储量)。供 JEI "+" 模板挑选一次性调用。
*/
public Map<AEItemKey, Long> snapshotStoredAmounts() {
Map<AEItemKey, Long> map = new HashMap<>();
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var entry : clientRepo.getAllEntries()) {
if (!(entry.getWhat() instanceof AEItemKey itemKey)) continue;
if (entry.getStoredAmount() > 0) {
map.put(itemKey, entry.getStoredAmount());
}
}
}
return map;
}
/**
* 构建网络中可自动合成物品的集合快照。供 JEI "+" 模板挑选一次性调用。
*/
public Set<AEItemKey> snapshotCraftableKeys() {
Set<AEItemKey> set = new HashSet<>();
var clientRepo = getClientRepo();
if (clientRepo != null) {
for (var entry : clientRepo.getAllEntries()) {
if (!(entry.getWhat() instanceof AEItemKey itemKey)) continue;
if (entry.isCraftable()) {
set.add(itemKey);
}
}
}
return set;
}
/** /**
* 检查 ME 网络中是否有指定材料的库存(含已预留量)。 * 检查 ME 网络中是否有指定材料的库存(含已预留量)。
* 直接遍历 clientRepo 条目匹配 ingredient,确保不受 linkStatus 时序或 getByIngredient 实现细节影响。 * 直接遍历 clientRepo 条目匹配 ingredient,确保不受 linkStatus 时序或 getByIngredient 实现细节影响。
@@ -23,7 +23,7 @@ import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList; 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.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 +98,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 拖拽设置模板)
@@ -243,8 +233,11 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
/** /**
* Encode 按钮可用的条件:源物品已配置 + 已选要素 + 空白样板槽有样板 + 成品槽为空。 * Encode 按钮可用的条件:源物品已配置 + 已选要素 + 空白样板槽有样板 + 成品槽为空。
* 服务端从 Tile 读(权威);客户端 encoder 为 null,从同步的槽内容判断。 * 服务端从 Tile 读(权威);客户端 encoder 为 null,从同步的槽内容判断。
* 先执行 getSourceAspects():重开 GUI 后源物品仍在 Tile 中,但本容器缓存为空,
* 不先解析会误判"未选要素"导致按钮置灰/编码无效。
*/ */
public boolean canEncode() { public boolean canEncode() {
getSourceAspects();
if (getSelectedAspect() == null) return false; if (getSelectedAspect() == null) return false;
if (getSourceItem().isEmpty()) return false; if (getSourceItem().isEmpty()) return false;
if (encoder != null) { if (encoder != null) {
@@ -260,10 +253,12 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
// 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板 // 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板
if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) { if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) {
ThaumicEnergistics.LOG.info("[DistillationEncoder] 玩家未解锁研究 {},拒绝编码蒸馏样板", RESEARCH_DISTILESSENTIA);
return; return;
} }
// 重开 GUI 后服务端缓存为空:先从 Tile 的源物品重新解析要素,
// 否则 getSelectedAspect() 恒为 null(客户端已选、服务端无缓存 → 点击 Encode 无效)。
getSourceAspects();
Aspect selected = getSelectedAspect(); Aspect selected = getSelectedAspect();
ItemStack source = getSourceItem(); ItemStack source = getSourceItem();
ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS); ItemStack blankSlot = encoder.inventory.getStackInSlot(TileDistillationPatternEncoder.SLOT_BLANK_PATTERNS);
@@ -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); }
}
@@ -11,6 +11,9 @@ import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.items.IItemHandler; import net.neoforged.neoforge.items.IItemHandler;
import net.neoforged.neoforge.items.ItemStackHandler; import net.neoforged.neoforge.items.ItemStackHandler;
import net.neoforged.neoforge.items.SlotItemHandler; import net.neoforged.neoforge.items.SlotItemHandler;
import appeng.api.upgrades.IUpgradeInventory;
import appeng.api.upgrades.UpgradeInventories;
import appeng.core.definitions.AEItems;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList; import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaContainerItem; import thaumcraft.api.aspects.IEssentiaContainerItem;
@@ -32,6 +35,11 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
private static final int PLAYER_INV_END = 28; private static final int PLAYER_INV_END = 28;
private static final int HOTBAR_START = 28; private static final int HOTBAR_START = 28;
private static final int HOTBAR_END = 37; private static final int HOTBAR_END = 37;
/** cell 升级卡槽(3 格,容器槽位 37..39,位于玩家槽之后)。 */
private static final int UPGRADE_X = 98;
private static final int UPGRADE_Y = 8;
private static final int UPGRADE_COUNT = 3;
private static final int UPGRADE_START = HOTBAR_END;
private final TileEssentiaCellWorkbench workbench; private final TileEssentiaCellWorkbench workbench;
@@ -50,6 +58,80 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
addSlot(new SlotItemHandler(cellHandler, 0, 152, 8)); addSlot(new SlotItemHandler(cellHandler, 0, 152, 8));
for (int r = 0; r < 3; r++) for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c + r * 9 + 9, 8 + c * 18, 167 + r * 18)); for (int r = 0; r < 3; r++) for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c + r * 9 + 9, 8 + c * 18, 167 + r * 18));
for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c, 8 + c * 18, 225)); for (int c = 0; c < 9; c++) addSlot(new Slot(inv, c, 8 + c * 18, 225));
// ===== cell 升级卡槽(3 格,动态绑定当前 cell 的升级库存,卡片持久化在 cell 物品上) =====
IItemHandler cellUpgrades = new CellUpgradeHandler(this::currentCellUpgrades);
for (int i = 0; i < UPGRADE_COUNT; i++) {
addSlot(new SlotItemHandler(cellUpgrades, i, UPGRADE_X + i * 18, UPGRADE_Y) {
@Override
public int getMaxStackSize() {
return 1;
}
@Override
public boolean mayPlace(ItemStack s) {
return AEItems.CAPACITY_CARD.is(s) || AEItems.FUZZY_CARD.is(s);
}
@Override
public void setChanged() {
super.setChanged();
if (workbench != null) {
workbench.setChanged();
}
}
});
}
}
/** 当前 cell 的升级库存;无 cell 时返回空库存。 */
private IUpgradeInventory currentCellUpgrades() {
if (workbench != null && workbench.hasCell()) {
ItemStack cell = workbench.getCell();
if (cell.getItem() instanceof ItemEssentiaCell cellItem) {
return cellItem.getUpgrades(cell);
}
}
return UpgradeInventories.empty();
}
/** 把 AE2 InternalInventory(IUpgradeInventory)适配成 NeoForge IItemHandler,供 SlotItemHandler 使用。 */
private static final class CellUpgradeHandler implements IItemHandler {
private final java.util.function.Supplier<IUpgradeInventory> supplier;
CellUpgradeHandler(java.util.function.Supplier<IUpgradeInventory> supplier) {
this.supplier = supplier;
}
@Override
public int getSlots() {
return supplier.get().size();
}
@Override
public ItemStack getStackInSlot(int slot) {
return supplier.get().getStackInSlot(slot);
}
@Override
public ItemStack insertItem(int slot, ItemStack stack, boolean simulate) {
return supplier.get().insertItem(slot, stack, simulate);
}
@Override
public ItemStack extractItem(int slot, int amount, boolean simulate) {
return supplier.get().extractItem(slot, amount, simulate);
}
@Override
public int getSlotLimit(int slot) {
return supplier.get().getSlotLimit(slot);
}
@Override
public boolean isItemValid(int slot, ItemStack stack) {
return supplier.get().isItemValid(slot, stack);
}
} }
/** 客户端打开菜单用(IMenuTypeExtension 注册);workbench 为 null,分区操作走 C2S 到服务端。 */ /** 客户端打开菜单用(IMenuTypeExtension 注册);workbench 为 null,分区操作走 C2S 到服务端。 */
@@ -134,6 +216,9 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
// 玩家背包 → cell 槽(仅当 cell 槽空且物品是源质元件) // 玩家背包 → cell 槽(仅当 cell 槽空且物品是源质元件)
if (stack.getItem() instanceof ItemEssentiaCell && workbench.getCell().isEmpty()) { if (stack.getItem() instanceof ItemEssentiaCell && workbench.getCell().isEmpty()) {
if (!moveItemStackTo(stack, SLOT_CELL, SLOT_CELL + 1, false)) return ItemStack.EMPTY; if (!moveItemStackTo(stack, SLOT_CELL, SLOT_CELL + 1, false)) return ItemStack.EMPTY;
} else if (AEItems.CAPACITY_CARD.is(stack) || AEItems.FUZZY_CARD.is(stack)) {
// 容量/模糊卡 → cell 升级槽
if (!moveItemStackTo(stack, UPGRADE_START, UPGRADE_START + UPGRADE_COUNT, false)) return ItemStack.EMPTY;
} else { } else {
// 含源质物品 → 尝试添加分区(物品不消费,仅添加分区) // 含源质物品 → 尝试添加分区(物品不消费,仅添加分区)
ResourceLocation aspectId = getAspectFromItem(stack); ResourceLocation aspectId = getAspectFromItem(stack);
@@ -142,6 +227,9 @@ public class ContainerEssentiaCellWorkbench extends AbstractContainerMenu {
} }
return ItemStack.EMPTY; return ItemStack.EMPTY;
} }
} else {
// 升级卡槽 → 玩家背包
if (!moveItemStackTo(stack, PLAYER_INV_START, HOTBAR_END, true)) return ItemStack.EMPTY;
} }
if (stack.isEmpty()) { if (stack.isEmpty()) {
@@ -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;
@@ -32,7 +33,6 @@ import thaumicenergistics.common.tiles.TileKnowledgeInscriber;
import thaumicenergistics.init.ModItems; import thaumicenergistics.init.ModItems;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.ThaumicEnergistics;
public class ContainerKnowledgeInscriber extends AbstractContainerMenu { public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
@@ -121,12 +121,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);
@@ -137,6 +132,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
kCoreHandler.open(slotStack); kCoreHandler.open(slotStack);
updatePatternSlots(); updatePatternSlots();
} }
// 服务端构造后立即同步按钮状态到客户端(否则客户端 saveStateSlot 一直为初始 0 → 按钮永远无效)
sendSaveState();
} }
private void initPatternSlots() { private void initPatternSlots() {
@@ -185,6 +182,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
} }
syncKCoreToTile(); syncKCoreToTile();
// 放/取核心后同步按钮状态(可保存/可删除/无效)到客户端
sendSaveState();
} }
private void clearPatternSlots() { private void clearPatternSlots() {
@@ -376,7 +375,6 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
String research = activeRecipe.getResearch(); String research = activeRecipe.getResearch();
if (research != null && !research.isEmpty() if (research != null && !research.isEmpty()
&& !TCResearchManager.has(playerInv.player, research)) { && !TCResearchManager.has(playerInv.player, research)) {
ThaumicEnergistics.LOG.info("[KnowledgeInscriber] 玩家未解锁研究 {},拒绝保存奥术样板", research);
return; return;
} }
@@ -0,0 +1,223 @@
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.ThEEssentiaContainers;
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;
}
// 罐子(JarBlockItem):每次只装 1 个空罐(手堆叠 -1,背包得 1 满罐)。
// 其余 IEssentiaContainerItem(天域之华等)不参与装罐。
if (!ThEEssentiaContainers.isJar(carried)) {
return false;
}
IEssentiaContainerItem container = (IEssentiaContainerItem) carried.getItem();
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) {
// 仅要素安瓿/要素罐子(天域之华、水晶等其他 IEssentiaContainerItem 一律不处理)
if (!ThEEssentiaContainers.isEssentiaContainer(stack)) {
return null;
}
if (!(stack.getItem() instanceof IEssentiaContainerItem container)) {
return stack; // 空玻璃安瓿:无源质可存
}
int count = stack.getCount();
if (count <= 0) {
return stack;
}
AspectList aspects = container.getAspects(stack);
if (aspects == null || aspects.isEmpty()) {
return stack;
}
// 阶段1:预检(SIMULATE 无副作用)——网络必须能完整容纳全部源质才继续;
// 任一项放不下则整个堆叠原样保留,避免"部分注入后罐子被消耗、剩余源质凭空消失"。
Aspect[] aspectArr = aspects.getAspects();
ResourceLocation[] ids = new ResourceLocation[aspectArr.length];
long[] totals = new long[aspectArr.length];
int n = 0;
for (Aspect aspect : aspectArr) {
if (aspect == null) {
continue;
}
int perItem = aspects.getAmount(aspect);
if (perItem <= 0) {
continue;
}
ResourceLocation aspectId = TCReflection.getAspectId(aspect);
if (aspectId == null) {
continue;
}
long total = (long) perItem * count;
if (storage.insert(AEssentiaKey.of(aspectId), total,
Actionable.SIMULATE, actionSource) < total) {
return stack; // 网络容量不足:完全不注入,罐子/安瓿原样保留
}
ids[n] = aspectId;
totals[n] = total;
n++;
}
if (n == 0) {
return stack;
}
// 阶段2:执行注入(预检已保证容量;若能量不足导致未完全注入,则回滚本次及此前
// 已注入的量并保留罐子,保证网络数量与罐子状态一致——不丢源质也不复制源质)
for (int i = 0; i < n; i++) {
long injected = StorageHelper.poweredInsert(
energySource, storage, AEssentiaKey.of(ids[i]), totals[i], actionSource);
if (injected < totals[i]) {
if (injected > 0) {
storage.insert(AEssentiaKey.of(ids[i]), injected,
Actionable.MODULATE, actionSource);
}
for (int j = 0; j < i; j++) {
storage.insert(AEssentiaKey.of(ids[j]), totals[j],
Actionable.MODULATE, actionSource);
}
return stack;
}
}
// 全部注入成功则消耗整个堆叠(罐子留空罐、安瓿返回空 glass_phial)
if (stack.getItem() instanceof JarBlockItem) {
return new ItemStack(stack.getItem());
}
// 安瓿:返回空安瓿(glass_phial),不直接消失
ItemStack emptyPhial = new ItemStack(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")), count);
return emptyPhial;
}
/** 生成装满的安瓿(phial_of_essentia_&lt;tag&gt;),与 TC JarBlock.filledPhialStack 一致。 */
private static ItemStack filledPhialStack(String aspectTag) {
ResourceLocation id = ResourceLocation.fromNamespaceAndPath(
"thaumcraft", "phial_of_essentia_" + aspectTag);
net.minecraft.world.item.Item item = BuiltInRegistries.ITEM.getOptional(id)
.orElse(BuiltInRegistries.ITEM.get(
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")));
return new ItemStack(item);
}
}
@@ -0,0 +1,43 @@
package thaumicenergistics.common.container;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack;
import net.minecraft.resources.ResourceLocation;
import appeng.api.storage.ITerminalHost;
import appeng.menu.me.common.MEStorageMenu;
/**
* 源质终端的菜单(有线端与无线端共用)。
* <p>
* 继承 AE2 原版 {@link MEStorageMenu} 的全部普通物品行为(左键/Shift+点击/拖拽均按普通物品处理)。
* 源质的存入与取出统一由右键交互承担:
* <ul>
* <li>手持装有源质的罐子/安瓿 + 右键任意处 → 源质存入网络({@link EssentiaFillHelper#extractContainerContent})。</li>
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出装罐({@link #fillContainerFromGrid})。</li>
* </ul>
*/
public class EssentiaTerminalMenu extends MEStorageMenu {
public EssentiaTerminalMenu(MenuType<?> menuType, int id, Inventory ip, ITerminalHost host) {
super(menuType, id, ip, host);
}
/**
* 右键终端源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。
* 具体逻辑见 {@link EssentiaFillHelper#fillContainerFromGrid}。
*
* @param aspectId 源质 aspect 的 ResourceLocation
* @return true = 处理成功(装罐/装安瓿),false = 无法处理
*/
public boolean fillContainerFromGrid(ResourceLocation aspectId) {
if (isClientSide()) {
return false;
}
var player = getPlayerInventory().player;
var carried = getCarried();
return EssentiaFillHelper.fillContainerFromGrid(
this.storage, this.energySource, getActionSource(), player, carried, aspectId);
}
}
@@ -6,42 +6,54 @@ import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.ContainerData; import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import appeng.client.gui.Icon;
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 整张背景。
* 第1块 主矩形:(0,0) → 176×197 * 进度条填充材质在纹理 y=240(7 条 4×16,x 步长 5:0,5,10,15,20,25,30)。
* 第2块 右上不规则:(179,0) → 上部32w(0~15y),下部55w(16~103y)
* 第3块 右下护甲区:(180,129) → 68×68
* 第4块 底部7个Vis条:每个 4×16 像素,起始(42,202),步长18(42-45, 60-63, 78-81...)
*
* 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制,
* 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。
*/ */
@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(
ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png"); ThaumicEnergistics.MODID, "textures/gui/arcane_assembler.png");
// ===== 第4块 Vis条 ===== private static final int BAR_W = 4;
/** 纹理中空槽(暗色背景)的 Y 坐标 = 进度条在 GUI 中的显示位置 */ private static final int BAR_H = 16;
private static final int VIS_EMPTY_Y = 87; /** 进度条填充材质纹理 y。 */
/** 纹理中满亮条的 Y 坐标(裁剪源,不直接显示) */ private static final int BAR_FILL_Y = 240;
private static final int VIS_FULL_Y = 202;
private static final int VIS_BAR_W = 4; /** 7 条进度条:{填充材质 x, 目标 x, 目标 y},顺序 风/水/火/秩序/混沌/地/合成。 */
private static final int VIS_BAR_H = 16; private static final int[][] BARS = {
private static final int VIS_BAR_STEP = 18; { 0, 8, 81 }, // 风 AIR
/** 贴图规范起始 X = 41(步长18:41,59,77,95,113,131,149) */ { 5, 8, 99 }, // 水 WATER
private static final int VIS_BAR_X = 41; { 10, 8, 117 }, // 火 FIRE
{ 15, 15, 81 }, // 秩序 ORDER
{ 20, 15, 99 }, // 混沌 ENTROPY
{ 25, 15, 117 },// 地 EARTH
{ 30, 141, 99 } // 合成条
};
/** 配方预览 3×3 起点(16×16 格,步距 18)。 */
private static final int PREVIEW_X = 26;
private static final int PREVIEW_Y = 81;
public GuiArcaneAssembler(ContainerArcaneAssembler menu, Inventory inv, Component title) { public GuiArcaneAssembler(ContainerArcaneAssembler menu, Inventory inv, Component title) {
super(menu, inv, title); super(menu, inv, title);
this.imageWidth = 254; // 主区按 0~175 取中轴线 87.5 居中(imageWidth=175,MC 自动按 175 居中);
this.imageHeight = 226; // 右侧 176~196 作为附加(右上角块),blit 整图时自然延伸,不占主区宽度。
this.inventoryLabelY = 115 - 12; this.imageWidth = 175;
this.titleLabelY = 5; this.imageHeight = 231;
// 物品栏标题下移(136→137)
this.inventoryLabelY = 137;
// 奥术装配器标题上移(4→3)
this.titleLabelY = 3;
} }
@Override @Override
@@ -49,42 +61,50 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
int left = (this.width - this.imageWidth) / 2; int left = (this.width - this.imageWidth) / 2;
int top = (this.height - this.imageHeight) / 2; int top = (this.height - this.imageHeight) / 2;
// ===== 分块绘制纹理(仿1.7.10,bar区域不参与blit) ===== // 整图渲染(0~196,含右侧右上角附加),主区 0~175 已居中
g.blit(TEX, left, top, 0, 0, 196, imageHeight);
// 第1块:主矩形 176×197 // ===== 空槽图标:加速卡(4 格) =====
g.blit(TEX, left, top, 0, 0, 176, 197); for (int i = 0; i < 4; i++) {
Slot slot = this.menu.getUpgradeSlot(i);
if (!slot.hasItem()) {
Icon.BACKGROUND_UPGRADE.getBlitter().dest(left + slot.x, top + slot.y).blit(g);
}
}
// 第2块:右上不规则区域(上部 32×16 + 下部 55×88) // ===== 配方预览:3×3 材料 + 预产物(数据由 AssemblerPreviewSyncPacket 同步到菜单) =====
g.blit(TEX, left + 179, top, 179, 0, 32, 16); if (this.menu.clientPreviewActive) {
g.blit(TEX, left + 179, top + 16, 179, 16, 55, 88); for (int i = 0; i < 9; i++) {
ItemStack stack = this.menu.clientPreviewGrid[i];
if (stack == null || stack.isEmpty()) continue;
int col = i % 3, row = i / 3;
g.renderItem(stack, left + PREVIEW_X + col * 18, top + PREVIEW_Y + row * 18);
}
// 预产物:16×16 居中于 21×21 大格子(113,96 ~ 134,117)
ItemStack result = this.menu.clientPreviewResult;
if (!result.isEmpty()) {
g.renderItem(result, left + 115, top + 98);
}
}
// 第3块:右下正方形 68×68 // ===== 进度条:6 要素 + 1 合成条(填充材质 y=240,按比例从底部向上裁剪) =====
g.blit(TEX, left + 180, top + 129, 180, 129, 68, 68);
// ===== 第4块:Vis条(1.7.10 裁剪方式:Y=87 空槽背景 + Y=202 亮条裁剪) =====
// 数据源:容器同步(menu.visData,服务端每 tick 同步 storedVis/isCrafting/进度——不依赖 BE 网络同步)
ContainerData data = this.menu.visData; ContainerData data = this.menu.visData;
final int maxCvis = 187 * 10; final int maxCvis = 187 * 10;
for (int i = 0; i < 7; i++) { for (int i = 0; i < BARS.length; i++) {
int bx = left + VIS_BAR_X + i * VIS_BAR_STEP; int srcX = BARS[i][0];
int by = top + VIS_EMPTY_Y; int bx = left + BARS[i][1];
int by = top + BARS[i][2];
// 从纹理 Y=87 裁剪空槽背景 float ratio;
g.blit(TEX, bx, by, VIS_BAR_X + i * VIS_BAR_STEP, VIS_EMPTY_Y, VIS_BAR_W, VIS_BAR_H);
float ratio = 0f;
if (i < 6) { if (i < 6) {
// 1-6格:源质充能比例
int cvis = data.get(ContainerArcaneAssembler.DATA_AIR + i); int cvis = data.get(ContainerArcaneAssembler.DATA_AIR + i);
ratio = Math.min(1f, (float) cvis / maxCvis); ratio = Math.min(1f, (float) cvis / maxCvis);
} else { } else {
// 第7格:非合成时要素接入常亮,合成时显示渐变进度
if (data.get(ContainerArcaneAssembler.DATA_CRAFTING) == 1) { if (data.get(ContainerArcaneAssembler.DATA_CRAFTING) == 1) {
int tick = data.get(ContainerArcaneAssembler.DATA_CRAFT_TICK); int tick = data.get(ContainerArcaneAssembler.DATA_CRAFT_TICK);
int total = Math.max(1, data.get(ContainerArcaneAssembler.DATA_TICKS_PER_CRAFT)); int total = Math.max(1, data.get(ContainerArcaneAssembler.DATA_TICKS_PER_CRAFT));
ratio = Math.min(1f, (float) tick / total); ratio = Math.min(1f, (float) tick / total);
} else { } else {
// 要素接入达成:任一原质充入(源质已从网络流入)→ 常亮
boolean anyVis = false; boolean anyVis = false;
for (int a = 0; a < 6; a++) { for (int a = 0; a < 6; a++) {
if (data.get(ContainerArcaneAssembler.DATA_AIR + a) > 0) { anyVis = true; break; } if (data.get(ContainerArcaneAssembler.DATA_AIR + a) > 0) { anyVis = true; break; }
@@ -93,20 +113,12 @@ public class GuiArcaneAssembler extends AbstractContainerScreen<ContainerArcaneA
} }
} }
// 从纹理 Y=202 裁剪满亮条,按高度百分比叠加 int fillHeight = Math.round(BAR_H * ratio);
int fillHeight = Math.round(VIS_BAR_H * ratio);
if (fillHeight > 0) { if (fillHeight > 0) {
int srcX = VIS_BAR_X + i * VIS_BAR_STEP; int srcY = BAR_FILL_Y + (BAR_H - fillHeight);
int srcY = VIS_FULL_Y + (VIS_BAR_H - fillHeight); int dstY = by + (BAR_H - fillHeight);
int dstY = by + (VIS_BAR_H - fillHeight); g.blit(TEX, bx, dstY, srcX, srcY, BAR_W, fillHeight);
g.blit(TEX, bx, dstY, srcX, srcY, VIS_BAR_W, fillHeight);
} }
// bar 边框
g.fill(bx - 1, by - 1, bx, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx + VIS_BAR_W, by - 1, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
g.fill(bx - 1, by - 1, bx + VIS_BAR_W + 1, by, 0xAA000000);
g.fill(bx - 1, by + VIS_BAR_H, bx + VIS_BAR_W + 1, by + VIS_BAR_H + 1, 0xAA000000);
} }
} }
@@ -3,35 +3,87 @@ package thaumicenergistics.common.container;
import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Inventory;
import appeng.api.config.ActionItems;
import appeng.client.gui.widgets.ActionButton;
import appeng.core.AEConfig;
import appeng.menu.slot.AppEngCraftingSlot; import appeng.menu.slot.AppEngCraftingSlot;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.client.gui.AspectGuiRenderer; import thaumcraft.client.gui.AspectGuiRenderer;
import appeng.client.gui.me.common.MEStorageScreen; import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.style.ScreenStyle; import appeng.client.gui.style.ScreenStyle;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.ArcaneTerminalClearC2SPacket;
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;
public GuiArcaneCraftingTerminal(ContainerArcaneCraftingTerminal menu, Inventory playerInventory, public GuiArcaneCraftingTerminal(ContainerArcaneCraftingTerminal menu, Inventory playerInventory,
Component title, ScreenStyle style) { Component title, ScreenStyle style) {
super(menu, playerInventory, title, style); super(menu, playerInventory, title, style);
// 一键存入 ME 网络(合成格清空)
ActionButton clearBtn = new ActionButton(ActionItems.S_STASH, btn -> menu.clearCraftingGrid());
clearBtn.setHalfSize(true);
clearBtn.setDisableBackground(true);
widgets.add("clearCraftingGrid", clearBtn);
// 一键存入玩家背包(合成格清空)
ActionButton clearToPlayerInvBtn = new ActionButton(ActionItems.S_STASH_TO_PLAYER_INV,
btn -> menu.clearToPlayerInventory());
clearToPlayerInvBtn.setHalfSize(true);
clearToPlayerInvBtn.setDisableBackground(true);
widgets.add("clearToPlayerInv", clearToPlayerInvBtn);
// 打开终端时上报 AE2 设置"关闭时自动清理合成槽"——服务端在菜单 removed 时兜底清理
sendClearOnCloseSetting();
} }
/** 合成结果槽锚点 Y(首次计算缓存,避免每帧遍历 slots)。 */ /**
private int cachedAnchorY = -1; * 主区(0~175)按中轴线 87.5 居中:AE2 终端 imageWidth=194(srcRect),默认 leftPos 按 194 居中会让主区偏左约 10px。
* 覆写 leftPos 用 175 居中,右侧 176~194(view cells 区)作为附加延伸到右侧。
* <p>
* 关键时序:AEBaseScreen.init 先调 AbstractContainerScreen.init(按 imageWidth 算 leftPos),随后用该 leftPos
* 一次性解析并冻结按钮/文本层坐标(populateScreen);而渲染时贴图与槽位按当前 leftPos 画。若把 175 居中放在
* super.init() 之后,按钮层基准是 194 居中、贴图层是 175 居中,差 (194-175)/2≈10px,按钮层就像整体左移。
* 因此先把 imageWidth 临时改为 175,让 super.init() 全程内部就用 87.5 居中解析所有层,再恢复。
*/
@Override
public void init() {
int originalImageWidth = this.imageWidth;
this.imageWidth = 175;
super.init();
this.imageWidth = originalImageWidth;
this.leftPos = (this.width - 175) / 2;
}
/** 向服务端上报当前 AE2 全局设置"关闭时自动清理合成槽"。 */
private void sendClearOnCloseSetting() {
boolean enabled = AEConfig.instance().isClearGridOnClose();
PacketDistributor.sendToServer(new ArcaneTerminalClearC2SPacket(
this.menu.containerId, ArcaneTerminalClearC2SPacket.ACTION_SET_CLEAR_ON_CLOSE, enabled));
}
/**
* 关闭终端时按 AE2 全局设置"自动清理合成槽"清空合成网格。
* 双保险:先上报最新开关值,若开启再立即清一次(removed 兜底仍会执行,网格已空则无副作用)。
*/
@Override
public void onClose() {
sendClearOnCloseSetting();
if (AEConfig.instance().isClearGridOnClose()) {
this.getMenu().clearCraftingGrid();
}
super.onClose();
}
@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) {
@@ -42,22 +94,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;
@@ -15,6 +15,10 @@ public class GuiDistillationPatternEncoder extends AbstractContainerScreen<Conta
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath( private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/distillation_encoder.png"); ThaumicEnergistics.MODID, "textures/gui/distillation_encoder.png");
/** Encode 按钮材质:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。 */
private static final ResourceLocation BUTTON_TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/button.png");
private static final int GUI_WIDTH = 176; private static final int GUI_WIDTH = 176;
private static final int GUI_HEIGHT = 234; private static final int GUI_HEIGHT = 234;
@@ -35,11 +39,30 @@ public class GuiDistillationPatternEncoder extends AbstractContainerScreen<Conta
int left = (width - imageWidth) / 2; int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2; int top = (height - imageHeight) / 2;
// Encode 按钮:对齐 1.7.10(146,94),位于 encoded 槽上方,不覆盖 selected 槽 (116,69); // Encode 按钮:对齐 1.7.10(146,94),位于 encoded 槽上方;用 button.png 材质(常态/悬停),等比缩放容纳文字。
// 仅当配置完成(源物品+选中要素+空白样板就绪)时可点击 // 仅当配置完成(源物品+选中要素+空白样板就绪)时可点击
encodeButton = Button.builder(Component.literal("Encode"), btn -> { encodeButton = new Button(
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0); left + 140, top + 94, 34, 14, Component.translatable("thaumicenergistics.gui.encode"),
}).pos(left + 140, top + 94).size(34, 16).build(); btn -> this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0),
b -> Component.empty()) {
@Override
protected void renderWidget(GuiGraphics g, int mx, int my, float pt) {
// 仅鼠标悬停/按下显示第二帧(避免焦点导致一直蓝色)
int v = this.isHovered() ? 15 : 0;
// 源 (0,v,32,13) 等比拉伸到按钮 (34×14)
g.blit(BUTTON_TEX, this.getX(), this.getY(), this.width, this.height, 0, v, 32, 13, 32, 32);
// 按钮文字居中
String s = this.getMessage().getString();
if (!s.isEmpty()) {
var font = net.minecraft.client.Minecraft.getInstance().font;
int tw = font.width(s);
g.drawString(font, s,
this.getX() + (this.width - tw) / 2,
this.getY() + (this.height - 8) / 2,
0xFFFFFF, false);
}
}
};
addRenderableWidget(encodeButton); addRenderableWidget(encodeButton);
} }
@@ -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,95 @@
package thaumicenergistics.common.container;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import thaumicenergistics.common.integration.tc.ThEEssentiaContainers;
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 = ThEEssentiaContainers.isEssentiaContainer(sourceStack);
boolean containerHasEssentia = isContainer && ThEEssentiaContainers.hasEssentia(sourceStack);
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && this.hoveredSlot != null && this.menu.isPlayerSideSlot(this.hoveredSlot)) {
var slotStack = this.hoveredSlot.getItem();
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
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);
}
}
@@ -17,11 +17,15 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath( private static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/knowledge_inscriber.png"); ThaumicEnergistics.MODID, "textures/gui/knowledge_inscriber.png");
private static final int GUI_WIDTH = 210; /** 保存/删除样板按钮材质:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。 */
private static final ResourceLocation BUTTON_TEX = ResourceLocation.fromNamespaceAndPath(
ThaumicEnergistics.MODID, "textures/gui/button.png");
// 主区宽度 175(中轴线 87.5,主区对称居中);知识核心槽在右侧单独渲染(x=176~209 边栏),不计入主区宽度。
private static final int GUI_WIDTH = 175;
private static final int GUI_HEIGHT = 244; private static final int GUI_HEIGHT = 244;
private Button saveButton; private Button saveButton;
private Button clearButton;
private ContainerKnowledgeInscriber.CoreSaveState saveState = private ContainerKnowledgeInscriber.CoreSaveState saveState =
ContainerKnowledgeInscriber.CoreSaveState.Disabled_MissingCore; ContainerKnowledgeInscriber.CoreSaveState.Disabled_MissingCore;
@@ -40,48 +44,65 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
int left = (width - imageWidth) / 2; int left = (width - imageWidth) / 2;
int top = (height - imageHeight) / 2; int top = (height - imageHeight) / 2;
saveButton = Button.builder(Component.literal(""), btn -> { // 保存/删除样板按钮:放预览产物槽(结果槽 116,108)上方,等比 32×13。
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0); // 按钮中心与结果槽中心(x=125)关于同一 y 轴对称 → 按钮 x = 125-16 = 109。
}).pos(left + 141, top + 109).size(38, 18).build(); // 材质 button.png:常态 (0,0,32,13),悬停/按下 (0,15,32,12)。
saveButton = new Button(
left + 109, top + 90, 32, 13, Component.empty(),
btn -> this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 0),
b -> Component.empty()) {
@Override
protected void renderWidget(GuiGraphics g, int mx, int my, float pt) {
// 仅鼠标悬停/按下显示第二帧;用 isHovered() 而非 isHoveredOrFocused()——
// 后者在按钮获得键盘焦点(点击后)时也 true,会导致写入配方后一直保持蓝色
int v = this.isHovered() ? 15 : 0;
g.blit(BUTTON_TEX, this.getX(), this.getY(), 0, v, this.width, this.height, 32, 32);
// 按钮文字居中
String s = this.getMessage().getString();
if (!s.isEmpty()) {
var font = net.minecraft.client.Minecraft.getInstance().font;
int tw = font.width(s);
g.drawString(font, s,
this.getX() + (this.width - tw) / 2,
this.getY() + (this.height - 8) / 2,
0xFFFFFF, false);
}
}
};
addRenderableWidget(saveButton); addRenderableWidget(saveButton);
// 立即设置按钮文字/激活(F11 全屏切换会重建按钮,init 里必须重设,否则文字丢失)
clearButton = Button.builder(Component.literal("C"), btn -> { updateSaveButton();
this.minecraft.gameMode.handleInventoryButtonClick(this.menu.containerId, 1);
}).pos(left + 80, top + 89).size(8, 8).build();
addRenderableWidget(clearButton);
} }
@Override @Override
protected void containerTick() { protected void containerTick() {
super.containerTick(); super.containerTick();
ContainerKnowledgeInscriber.CoreSaveState newState = menu.getCurrentSaveState(); saveState = menu.getCurrentSaveState();
if (newState != saveState) { // 每 tick 重设(而非仅状态变化时):F11 resize 重建按钮后也能恢复文字/激活
saveState = newState;
updateSaveButton(); updateSaveButton();
} }
}
private void updateSaveButton() { private void updateSaveButton() {
switch (saveState) { switch (saveState) {
case Disabled_MissingCore: case Disabled_MissingCore:
saveButton.active = false; saveButton.active = false;
saveButton.setMessage(Component.literal("No Core")); saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.no_core"));
break; break;
case Disabled_InvalidRecipe: case Disabled_InvalidRecipe:
saveButton.active = false; saveButton.active = false;
saveButton.setMessage(Component.literal("Invalid")); saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.invalid"));
break; break;
case Disabled_CoreFull: case Disabled_CoreFull:
saveButton.active = false; saveButton.active = false;
saveButton.setMessage(Component.literal("Full")); saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.full"));
break; break;
case Enabled_Save: case Enabled_Save:
saveButton.active = true; saveButton.active = true;
saveButton.setMessage(Component.literal("Save")); saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.save"));
break; break;
case Enabled_Delete: case Enabled_Delete:
saveButton.active = true; saveButton.active = true;
saveButton.setMessage(Component.literal("Delete")); saveButton.setMessage(Component.translatable("thaumicenergistics.gui.knowledge_inscriber.delete"));
break; break;
} }
} }
@@ -90,7 +111,10 @@ public class GuiKnowledgeInscriber extends AbstractContainerScreen<ContainerKnow
protected void renderBg(GuiGraphics g, float pt, int mx, int my) { protected void renderBg(GuiGraphics g, float pt, int mx, int my) {
int x = (width - imageWidth) / 2; int x = (width - imageWidth) / 2;
int y = (height - imageHeight) / 2; int y = (height - imageHeight) / 2;
g.blit(TEX, x, y, 0, 0, imageWidth, imageHeight); // 主区(0~175)对称居中
g.blit(TEX, x, y, 0, 0, GUI_WIDTH, imageHeight);
// 右侧知识核心槽边栏(x=176~209)单独渲染,不占主区宽度
g.blit(TEX, x + 176, y, 176, 0, 34, imageHeight);
} }
@Override @Override
@@ -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;
}
}
@@ -1,21 +0,0 @@
package thaumicenergistics.common.features;
/**
* 功能:奥术自动合成。
* 启用奥术装配器及相关合成样板。
* 自 1.7.10 FeatureAutocrafting_Arcane 移植(约 7k 行)。
*/
public class FeatureAutocraftingArcane extends ThEFeatureBase {
public FeatureAutocraftingArcane() { super("arcane_crafting_terminal"); }
@Override
protected void doRegister() {
// Arcane Assembler 方块经 ModBlocks 注册
// TODO: Register arcane crafting pattern encoder, crafting CPU integration
}
@Override
protected void doPostInit() {
// TODO: Register arcane crafting recipes, research entries
}
}
@@ -1,20 +0,0 @@
package thaumicenergistics.common.features;
/**
* 源质存储元件功能。
* 注册 cell 物品、元件工作台与存储组件物品。
* 自 1.7.10 FeatureCells 移植(约 12k 行)。
*/
public class FeatureCells extends ThEFeatureBase {
public FeatureCells() { super("cells"); }
@Override
protected void doRegister() {
// 物品经 init 包的 ModItems 注册——本功能校验其存在
}
@Override
protected void doPostInit() {
// TODO: Register cell recipes, research entries, cell workbench functionality
}
}
@@ -1,35 +0,0 @@
package thaumicenergistics.common.features;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.init.ModItems;
/**
* 功能:源质 IO 总线。
* 启用源质输入/输出/存储总线Part部件
* Part注册已移至模组主类ThaumicEnergistics构造方法中,
* 通过AE2官方PartHelper.registerPart() API完成,
* 本Feature负责功能开关、配方注册、研究数据等跨模组集成内容。
*/
public class FeatureEssentiaIOBuses extends ThEFeatureBase {
public FeatureEssentiaIOBuses() { super("io_buses"); }
@Override
protected void doRegister() {
// ===== Part核心注册已在ThaumicEnergistics构造方法中完成 =====
// 原因:AE2 PartHelper.registerPart()需要DeferredItem注册完成后立即调用,
// Feature.register()的调用时机同样满足,但为了集中管理Part注册入口,统一在主类中处理。
// ===== 以下登记本Feature涉及的物品,供跨模组引用追踪 =====
ThaumicEnergistics.LOG.info(" -> EssentiaImportBus PartItem: {} (ready)",
ModItems.ESSENTIA_IMPORT_BUS.getId());
ThaumicEnergistics.LOG.info(" -> EssentiaExportBus PartItem: {} (pending)",
ModItems.ESSENTIA_EXPORT_BUS.getId());
ThaumicEnergistics.LOG.info(" -> EssentiaStorageBus PartItem: {} (pending)",
ModItems.ESSENTIA_STORAGE_BUS.getId());
}
@Override
protected void doPostInit() {
// 后置初始化:注册神秘时代研究条目、AE2合成配方等
}
}
@@ -1,15 +0,0 @@
package thaumicenergistics.common.features;
/**
* 源质监控功能。
* 启用源质水平检测器、存储监控器与转换监控器部件。
* 自 1.7.10 FeatureEssentiaMonitoring 移植(约 6k 行)。
*/
public class FeatureEssentiaMonitoring extends ThEFeatureBase {
public FeatureEssentiaMonitoring() { super("monitoring"); }
@Override
protected void doRegister() {
// TODO: Register monitoring-related parts when AE2 part API is integrated
}
}
@@ -1,7 +0,0 @@
package thaumicenergistics.common.features;
/** 功能:源质供应器。从 AE 网络核心抽取源质。 */
public class FeatureEssentiaProvider extends ThEFeatureBase {
public FeatureEssentiaProvider() { super("essentia_provider"); }
@Override protected void doRegister() {}
}
@@ -1,21 +0,0 @@
package thaumicenergistics.common.features;
/**
* 功能:源质谐振仓。
* 启用基于源质的能量生产。
* 自 1.7.10 FeatureEssentiaVibrationChamber 移植(约 4k 行)。
*/
public class FeatureEssentiaVibrationChamber extends ThEFeatureBase {
public FeatureEssentiaVibrationChamber() { super("vibration_chamber"); }
@Override
protected void doRegister() {
// EVC 方块经 ModBlocks 注册
// TODO: Register fuel values for each essentia type
}
@Override
protected void doPostInit() {
// TODO: Register EVC recipe, research entry
}
}
@@ -1,11 +0,0 @@
package thaumicenergistics.common.features;
/** 功能:无线傀儡背包。启用傀儡访问 AE 网络。 */
public class FeatureGolemBackpack extends ThEFeatureBase {
public FeatureGolemBackpack() { super("golem_backpack"); }
@Override
protected void doRegister() {
// GridLinkables 注册在 commonSetup 阶段完成(需要物品已绑定)
}
}
@@ -1,16 +0,0 @@
package thaumicenergistics.common.features;
/**
* 功能:注魔供应器。
* 为 TC 注魔自动供应源质。
* 自 1.7.10 FeatureInfusionProvider 移植(约 4k 行)。
*/
public class FeatureInfusionProvider extends ThEFeatureBase {
public FeatureInfusionProvider() { super("infusion_provider"); }
@Override
protected void doRegister() {
// Infusion Provider 方块经 ModBlocks 注册
// TODO: Register infusion detection and essentia delivery logic
}
}
@@ -1,17 +0,0 @@
package thaumicenergistics.common.features;
import thaumicenergistics.ThaumicEnergistics;
public final class FeatureRegistry {
private FeatureRegistry() {}
private static final ThEFeatureBase[] FEATURES = {
new FeatureCells(), new FeatureEssentiaIOBuses(), new FeatureEssentiaMonitoring(),
new FeatureEssentiaVibrationChamber(), new FeatureInfusionProvider(),
new FeatureAutocraftingArcane(), new FeatureVisRelayInterface(),
new FeatureGolemBackpack(), new FeatureEssentiaProvider()
};
public static void registerAll() {
for (var f : FEATURES) { ThaumicEnergistics.LOG.info("Feature [{}] {}", f.getName(), f.isEnabled() ? "enabled":"disabled"); f.register(); }
}
public static void postInitAll() { for (var f : FEATURES) f.postInit(); }
}
@@ -1,17 +0,0 @@
package thaumicenergistics.common.features;
import appeng.api.features.P2PTunnelAttunement;
import thaumicenergistics.init.ModItems;
public class FeatureVisRelayInterface extends ThEFeatureBase {
public FeatureVisRelayInterface() { super("vis_interface"); }
@Override
protected void doRegister() {
P2PTunnelAttunement.registerAttunementTag(ModItems.VIS_INTERFACE.get());
}
@Override
protected void doPostInit() {
}
}
@@ -29,6 +29,8 @@ public final class RecipeRegistration {
public static InfusionRecipe ESSENTIA_PROVIDER; public static InfusionRecipe ESSENTIA_PROVIDER;
public static InfusionRecipe INFUSION_PROVIDER; public static InfusionRecipe INFUSION_PROVIDER;
public static InfusionRecipe ARCANE_ASSEMBLER; public static InfusionRecipe ARCANE_ASSEMBLER;
/** 韵魔观测器注魔配方。 */
public static InfusionRecipe INFUSION_MONITOR;
// 奥术有序配方 // 奥术有序配方
public static ShapedArcaneRecipe STORAGE_COMPONENT_1K; public static ShapedArcaneRecipe STORAGE_COMPONENT_1K;
public static ShapedArcaneRecipe STORAGE_COMPONENT_4K; public static ShapedArcaneRecipe STORAGE_COMPONENT_4K;
@@ -50,10 +52,16 @@ public final class RecipeRegistration {
public static ShapelessArcaneRecipe ARCANE_CRAFTING_TERMINAL; public static ShapelessArcaneRecipe ARCANE_CRAFTING_TERMINAL;
public static ShapelessArcaneRecipe VIS_INTERFACE; public static ShapelessArcaneRecipe VIS_INTERFACE;
public static ShapelessArcaneRecipe ESSENTIA_VIBRATION_CHAMBER; public static ShapelessArcaneRecipe ESSENTIA_VIBRATION_CHAMBER;
/** 源质无线接收器(无序奥术)。 */
public static ShapelessArcaneRecipe ESSENTIA_PROVIDER_CONNECTION;
/** 源质无线绑定工具(无序奥术)。 */
public static ShapelessArcaneRecipe WIRELESS_CONNECTOR;
/** 赛特斯石英复制(坩埚炼金配方——研究页关联)。 */ /** 赛特斯石英复制(坩埚炼金配方——研究页关联)。 */
public static CrucibleRecipe CERTUS_DUPE_RECIPE; public static CrucibleRecipe CERTUS_DUPE_RECIPE;
private static final TagKey<Item> WISP_ESSENCES = tag("thaumicenergistics", "wisp_essences"); private static final TagKey<Item> WISP_ESSENCES = tag("thaumicenergistics", "wisp_essences");
/** AE2 三种显示器(普通/暗/半暗)任一可用。 */
private static final TagKey<Item> MONITORS = tag("thaumicenergistics", "monitors");
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
private static TagKey<Item> tag(String ns, String path) { private static TagKey<Item> tag(String ns, String path) {
@@ -79,8 +87,9 @@ public final class RecipeRegistration {
new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.EXCHANGE, 5)); new AspectList().add(Aspect.CRYSTAL, 5).add(Aspect.EXCHANGE, 5));
} }
// ==================== 奥术有序配方 ====================
private static void registerShapedArcane() { private static void registerShapedArcane() {
STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE1K", stack(ModItems.STORAGE_COMPONENT_1K), STORAGE_COMPONENT_1K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_1K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1), new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 1),
"EQ ", "EQ ",
"QPQ", "QPQ",
@@ -89,7 +98,7 @@ public final class RecipeRegistration {
'Q', ae("certus_quartz_crystal"), 'Q', ae("certus_quartz_crystal"),
'P', ae("logic_processor")); 'P', ae("logic_processor"));
STORAGE_COMPONENT_4K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE4K", stack(ModItems.STORAGE_COMPONENT_4K), STORAGE_COMPONENT_4K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_4K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 2), new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 2),
"EPE", "EPE",
"1G1", "1G1",
@@ -99,7 +108,7 @@ public final class RecipeRegistration {
'1', stack(ModItems.STORAGE_COMPONENT_1K), '1', stack(ModItems.STORAGE_COMPONENT_1K),
'G', ae("quartz_glass")); 'G', ae("quartz_glass"));
STORAGE_COMPONENT_16K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE16K", stack(ModItems.STORAGE_COMPONENT_16K), STORAGE_COMPONENT_16K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_16K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 4), new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 4),
"SPE", "SPE",
"4G4", "4G4",
@@ -110,7 +119,7 @@ public final class RecipeRegistration {
'G', ae("quartz_glass"), 'G', ae("quartz_glass"),
'E', WISP_ESSENCES); 'E', WISP_ESSENCES);
STORAGE_COMPONENT_64K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE64K", stack(ModItems.STORAGE_COMPONENT_64K), STORAGE_COMPONENT_64K = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIASTORAGE", stack(ModItems.STORAGE_COMPONENT_64K),
new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 8), new AspectList().add(Aspect.FIRE, 3).add(Aspect.ORDER, 8),
"SPS", "SPS",
"6G6", "6G6",
@@ -126,7 +135,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"));
@@ -136,11 +145,11 @@ 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"));
GOLEM_WIFI_BACKPACK_RECIPE = ThaumcraftApi.addArcaneCraftingRecipe("GOLEMWIFI", stack(ModItems.GOLEM_WIFI_BACKPACK), GOLEM_WIFI_BACKPACK_RECIPE = ThaumcraftApi.addArcaneCraftingRecipe("GOLEMWIFIBACKPACK", stack(ModItems.GOLEM_WIFI_BACKPACK),
new AspectList().add(Aspect.AIR, 6).add(Aspect.FIRE, 4).add(Aspect.ORDER, 3), new AspectList().add(Aspect.AIR, 6).add(Aspect.FIRE, 4).add(Aspect.ORDER, 3),
"tIt", "tIt",
"nRn", "nRn",
@@ -154,22 +163,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),
@@ -180,7 +190,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),
@@ -188,85 +198,46 @@ 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', ae("monitor"), 'L', MONITORS,
'T', tc("thaumonomicon"), 'T', tc("thaumonomicon"),
'P', ae("logic_processor")); 'P', ae("logic_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
new AspectList().add(Aspect.WATER, 5).add(Aspect.EARTH, 5).add(Aspect.FIRE, 5).add(Aspect.AIR, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("dark_monitor"),
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER),
new AspectList().add(Aspect.WATER, 5).add(Aspect.EARTH, 5).add(Aspect.FIRE, 5).add(Aspect.AIR, 5).add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
"ILI",
" T ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("semi_dark_monitor"),
'T', tc("thaumonomicon"),
'P', ae("logic_processor"));
DISTILLATION_ENCODER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER), DISTILLATION_ENCODER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3), new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI", "ILI",
" G ", " G ",
"IPI", "IPI",
'I', new ItemStack(Items.IRON_INGOT, 4), 'I', new ItemStack(Items.IRON_INGOT, 1),
'L', ae("monitor"), 'L', MONITORS,
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("dark_monitor"),
'G', tc("thaumometer"),
'P', ae("engineering_processor"));
ThaumcraftApi.addArcaneCraftingRecipe("DISTILLATIONENCODER", stack(ModBlocks.DISTILLATION_ENCODER),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5).add(Aspect.FIRE, 3),
"ILI",
" G ",
"IPI",
'I', new ItemStack(Items.IRON_INGOT, 4),
'L', ae("semi_dark_monitor"),
'G', tc("thaumometer"), 'G', tc("thaumometer"),
'P', ae("engineering_processor")); 'P', ae("engineering_processor"));
} }
// ==================== 奥术无序配方 ====================
private static void registerShapelessArcane() { private static void registerShapelessArcane() {
// 1. 扩散核心 // 1. 扩散核心
DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE), DIFFUSION_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.DIFFUSION_CORE),
new AspectList().add(Aspect.WATER, 2).add(Aspect.ENTROPY, 2), new AspectList().add(Aspect.WATER, 2).add(Aspect.ENTROPY, 2),
tc("quicksilver"), tc("quicksilver_drop"),
tc("quicksilver"), tc("quicksilver_drop"),
tc("quicksilver"), tc("quicksilver_drop"),
tc("entropy_shard"), tc("entropy_shard"),
ae("annihilation_core")); ae("annihilation_core"));
// 2. 凝聚核心 // 2. 凝聚核心
COALESCENCE_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.COALESCENCE_CORE), COALESCENCE_CORE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("DIGISENTIA", stack(ModItems.COALESCENCE_CORE),
new AspectList().add(Aspect.WATER, 2).add(Aspect.ORDER, 2), new AspectList().add(Aspect.WATER, 2).add(Aspect.ORDER, 2),
tc("quicksilver"), tc("quicksilver_drop"),
tc("quicksilver"), tc("quicksilver_drop"),
tc("quicksilver"), tc("quicksilver_drop"),
tc("order_shard"), tc("order_shard"),
ae("formation_core")); ae("formation_core"));
// 3. 原质等级发射器 // 3. 原质等级发射器
ESSENTIA_LEVEL_EMITTER = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAMONITORING", new ItemStack(ModItems.ESSENTIA_LEVEL_EMITTER.get(), 1), ESSENTIA_LEVEL_EMITTER = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", new ItemStack(ModItems.ESSENTIA_LEVEL_EMITTER.get(), 1),
new AspectList().add(Aspect.FIRE, 4), new AspectList().add(Aspect.FIRE, 4),
ae("calculation_processor"), ae("calculation_processor"),
new ItemStack(Items.REDSTONE_TORCH), new ItemStack(Items.REDSTONE_TORCH),
@@ -275,26 +246,7 @@ public final class RecipeRegistration {
// 4. 原质终端 // 4. 原质终端
ESSENTIA_TERMINAL = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM), ESSENTIA_TERMINAL = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1), new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一 MONITORS,
ae("monitor"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"),
tc("vis_filter"));
ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一
ae("dark_monitor"),
stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"),
tc("vis_filter"));
ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIATERMINAL", stack(ModItems.ESSENTIA_TERMINAL_ITEM),
new AspectList().add(Aspect.WATER, 5).add(Aspect.ORDER, 2).add(Aspect.FIRE, 1),
//'ae2:semi_dark_monitor' 'ae2:monitor' 'ae2:dark_monitor'三者任一
ae("semi_dark_monitor"),
stack(ModItems.DIFFUSION_CORE), stack(ModItems.DIFFUSION_CORE),
stack(ModItems.COALESCENCE_CORE), stack(ModItems.COALESCENCE_CORE),
ae("logic_processor"), ae("logic_processor"),
@@ -310,7 +262,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. 原质振荡室
@@ -320,6 +272,22 @@ public final class RecipeRegistration {
tc("jar_of_essentia"), tc("jar_of_essentia"),
stack(ModItems.COALESCENCE_CORE)); stack(ModItems.COALESCENCE_CORE));
// 8. 源质无线接收器(无序奥术,研究:源质供应器)
ESSENTIA_PROVIDER_CONNECTION = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAPROVIDER",
new ItemStack(ModBlocks.ESSENTIA_PROVIDER_CONNECTION.get(), 2),
new AspectList().add(Aspect.ORDER, 8).add(Aspect.AIR, 6).add(Aspect.FIRE, 3),
stack(ModItems.COALESCENCE_CORE),
stack(ModItems.DIFFUSION_CORE),
ae("wireless_receiver"),
tc("balanced_shard"));
// 9. 源质无线绑定工具(无序奥术,研究:源质供应器)
WIRELESS_CONNECTOR = ThaumcraftApi.addShapelessArcaneCraftingRecipe("ESSENTIAPROVIDER",
stack(ModItems.WIRELESS_CONNECTOR),
new AspectList().add(Aspect.ORDER, 5).add(Aspect.ENTROPY, 5),
ae("certus_quartz_wrench"),
tc("balanced_shard"));
} }
@@ -351,27 +319,51 @@ 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());
// 4. 韵魔观测器
// 中央:原版侦测器;外围:元始护符 + 揭示护目镜 + 世界盐×2。
// 需要灵气:魔力×16、灵气(AURA)×32、感官(SENSES, tag=sensus)×16。
// 不稳定度 3(配方基础,参与"总不稳定 = 基础 + 祭坛对称度",可自由调整:
// 1~2 低风险 / 3~5 中等 / 6~8 高)。
INFUSION_MONITOR = ThaumcraftApi.addInfusionCraftingRecipe("INFUSIONMONITOR", stack(ModBlocks.INFUSION_MONITOR),
3, new AspectList().add(Aspect.MAGIC, 16).add(Aspect.AURA, 32).add(Aspect.SENSES, 16),
new ItemStack(Items.OBSERVER),
tc("primal_charm"),
tc("goggles_revealing"),
tc("salis_mundus"),
tc("salis_mundus"));
} }
/**
* 生成"满充能"银木神秘权杖作为注魔配方的预期组件。
*
* ⚠ 关键:不要调用 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);
@@ -1,43 +0,0 @@
package thaumicenergistics.common.features;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.config.ThEConfig;
import java.util.function.Supplier;
/**
* 所有功能模块的基类。每个功能可独立启停。
* 自 1.7.10 ThEFeatureBase 移植。
*/
public abstract class ThEFeatureBase {
private final String name;
protected boolean enabled;
protected ThEFeatureBase(String name) {
this.name = name;
this.enabled = ThEConfig.isFeatureEnabled(name);
}
public final String getName() { return name; }
public final boolean isEnabled() { return enabled; }
/** 模组构造期间调用,用于注册物品与方块。 */
public void register() {
if (!enabled) return;
ThaumicEnergistics.LOG.info("Feature [{}] loading...", name);
doRegister();
}
/** 所有功能注册完成后调用,用于跨功能设置。 */
public void postInit() {
if (!enabled) return;
doPostInit();
}
/** 覆写以注册本功能特有的方块/物品。 */
protected abstract void doRegister();
/** 覆写以进行注册后设置(配方、研究等)。 */
protected void doPostInit() {}
}
@@ -1,22 +0,0 @@
package thaumicenergistics.common.grid;
/**
* 表示连接到 AE2 网格的部件。
* 对应 1.7.10 AEPartGridBlock——为 1.21.1 简化。
* TODO: 通过 AE2 19.x API 完整连接 IGridNode。
*/
public class AEPartGridBlock {
private final Object part; // 占位 PartBase 或 IPartItem
public AEPartGridBlock(Object part) { this.part = part; }
public double getIdlePowerUsage() {
// TODO: Delegate to part
return 1.0;
}
public Object getGridNode() {
// TODO: AE2 IGridNode
return null;
}
}
@@ -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;
@@ -21,8 +21,11 @@ import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/** /**
* 源质网格服务的实现。查询和提取源质时同时检查两个存储后端: * 源质网格服务的实现。查询和提取源质时同时检查两个存储后端:
* Path A: EssentiaMonitor(注魔供应器/傀儡背包直接写入), * Path A: EssentiaMonitor(历史遗留的内存镜像,现仅参与读取),
* Path B: AE2 原生存储(源质存储元件、输入总线等)。 * Path B: AE2 原生存储(源质存储元件、存储总线等)。
* <p>
* 写入只走 Path B:网络装不下的部分如实返回"未接受",不再回退进内存 monitor
* (否则网络没有源质存储时也会照吸,源质既不在 ME 存储里也不持久化,等于消失)。
*/ */
public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider { public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider {
@@ -101,11 +104,23 @@ 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;
if (!simulate) { // 只承认 AE2 原生存储(源质存储元件 / 存储总线)真正接收的量。
long current = monitor.get(aspectId); // 网络没有源质存储或已满时如实返回"未接受",调用方(输入总线 / 无线接收器 /
monitor.set(aspectId, current + amount); // 源质供应器 / 傀儡背包)据此把源质留在原容器里,不抽走。
// 旧实现把装不下的部分塞进内存 monitor 兜底 —— 等于"网络无存储也照吸",
// 而这些源质不在任何 ME 存储里、不随存档持久化,表现为源质凭空消失。
var nativeInjected = injectNative(aspectId, amount, simulate);
return amount - nativeInjected;
} }
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
@@ -0,0 +1,74 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* 奥术装配器 Jade 显示:
* <ul>
* <li>网络状态:在线(节点 active)/ 无频道(有能量)/ 离线(无能量)</li>
* <li>要素状态:已连接(内存卡配置了 vis 源频率)/ 未连接</li>
* </ul>
* 服务端把状态写入 NBT,客户端渲染。
*/
public enum AssemblerProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
private static final String TAG_VIS = "VisConnected";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileArcaneAssembler assembler) {
data.putBoolean(TAG_ONLINE, assembler.isActive());
data.putBoolean(TAG_POWERED, assembler.isNodePowered());
data.putBoolean(TAG_VIS, assembler.getVisSourceFreq() != 0);
}
}
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
boolean visConnected = accessor.getServerData().getBoolean(TAG_VIS);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
// 要素源状态:已连接(绿)/ 未连接(灰)
tooltip.add(Component.translatable("thaumicenergistics.jade.assembler.vis_source",
Component.translatable(visConnected
? "thaumicenergistics.jade.assembler.vis_connected"
: "thaumicenergistics.jade.assembler.vis_disconnected")
.withStyle(style -> style.withColor(visConnected ? 0xFF55FF55 : 0xFFAAAAAA))));
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "assembler");
}
}
@@ -0,0 +1,139 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileInfusionMonitor;
import thaumicenergistics.common.tiles.TileInfusionMonitor.RiskData;
/**
* 韵魔观测器 Jade 显示:
* <ol>
* <li>始终显示 ME 网络状态(在线绿 / 未连接红 / 无能量红 / 无频道橙)。</li>
* <li>仅网络在线时展示附近注魔祭坛的风险数据:5 档风险(纯文字,无星星)+ 不稳定度 + 档位描述。</li>
* </ol>
* 服务端把 BE 缓存的 RiskData 与节点状态写入 NBT,客户端渲染。
*/
public enum InfusionMonitorProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String KEY = "InfusionMonitorRisk";
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileInfusionMonitor monitor) {
data.putBoolean(TAG_ONLINE, monitor.isNetworkOnline());
data.putBoolean(TAG_POWERED, monitor.isNodePowered());
RiskData risk = monitor.getCachedRisk();
CompoundTag tag = new CompoundTag();
tag.putBoolean("found", risk.found());
tag.putBoolean("crafting", risk.crafting());
tag.putInt("tier", risk.tier());
tag.putInt("instability", risk.instability());
tag.putInt("symmetry", risk.symmetry());
tag.putInt("recipeInstability", risk.recipeInstability());
tag.putString("tierText", risk.tierText());
tag.putString("shortages", risk.shortages());
tag.putInt("totalNeeded", risk.totalNeeded());
data.put(KEY, tag);
}
}
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
// 标题:监视器
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.title"));
// 网络状态(始终显示):在线绿 / 无频道橙 / 离线红
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
// 未联网 → 不显示风险数据(风险缓存是每 tick 扫描的,与网络无关,未联网时不应展示)
if (!online) {
return;
}
if (!accessor.getServerData().contains(KEY)) return;
CompoundTag tag = accessor.getServerData().getCompound(KEY);
boolean found = tag.getBoolean("found");
if (!found) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.no_matrix"));
return;
}
boolean crafting = tag.getBoolean("crafting");
int tier = tag.getInt("tier");
int instability = tag.getInt("instability");
int symmetry = tag.getInt("symmetry");
int recipeInstability = tag.getInt("recipeInstability");
String tierText = tag.getString("tierText");
String shortages = tag.getString("shortages");
int totalNeeded = tag.getInt("totalNeeded");
// 风险档位:纯文字(极低/低/中/高/极高),按档位着色
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.tier",
Component.translatable("thaumicenergistics.jade.tier." + tier), tier)
.withStyle(style -> style.withColor(tierColor(tier))));
// 不稳定度(分解:基础 + 祭坛)
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.instability",
instability, recipeInstability, symmetry));
// 档位文字(后果描述)
if (!tierText.isEmpty()) {
tooltip.add(Component.literal(tierText));
}
// 源质缺口
if (crafting) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.crafting"));
}
if (!shortages.isEmpty()) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.shortages", shortages));
} else if (totalNeeded > 0) {
tooltip.add(Component.translatable("thaumicenergistics.jade.monitor.essence_ok"));
}
}
private static int tierColor(int tier) {
return switch (tier) {
case 1 -> 0xFF55FF55; // 绿
case 2 -> 0xFFAAFF55; // 黄绿
case 3 -> 0xFFFFFF55; // 黄
case 4 -> 0xFFFFAA55; // 橙
default -> 0xFFFF5555; // 红
};
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "infusion_monitor");
}
}
@@ -0,0 +1,65 @@
package thaumicenergistics.common.integration.jade;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.IServerDataProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.tiles.TileProviderBase;
/**
* 注魔供应器 / 源质供应器 Jade 显示:ME 网络连接状态。
* <ul>
* <li>在线:节点 active(有能量 + 有频道)——绿</li>
* <li>无频道:已接线上网但没分到频道——橙</li>
* <li>离线:未接线路 / 网络无能量——红</li>
* </ul>
* 服务端把节点状态写入 NBT,客户端渲染。
*/
public enum InfusionProviderProvider implements IBlockComponentProvider, IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final String TAG_ONLINE = "NetworkOnline";
private static final String TAG_POWERED = "NodePowered";
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof TileProviderBase provider) {
data.putBoolean(TAG_ONLINE, provider.isActive());
data.putBoolean(TAG_POWERED, provider.getMainNode().isPowered());
}
}
@Override
public void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
if (!accessor.getServerData().contains(TAG_ONLINE)) return;
boolean online = accessor.getServerData().getBoolean(TAG_ONLINE);
boolean powered = accessor.getServerData().getBoolean(TAG_POWERED);
String statusKey;
int color;
if (online) {
statusKey = "thaumicenergistics.jade.provider.online";
color = 0xFF55FF55;
} else if (powered) {
statusKey = "thaumicenergistics.jade.provider.no_channel";
color = 0xFFFFAA55;
} else {
statusKey = "thaumicenergistics.jade.provider.offline";
color = 0xFFFF5555;
}
tooltip.add(Component.translatable("thaumicenergistics.jade.provider.network",
Component.translatable(statusKey).withStyle(style -> style.withColor(color))));
}
@Override
public ResourceLocation getUid() {
return ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "provider_network");
}
}
@@ -5,7 +5,11 @@ import snownee.jade.api.IWailaClientRegistration;
import snownee.jade.api.IWailaCommonRegistration; import snownee.jade.api.IWailaCommonRegistration;
import snownee.jade.api.IWailaPlugin; import snownee.jade.api.IWailaPlugin;
import snownee.jade.api.WailaPlugin; import snownee.jade.api.WailaPlugin;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber; import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber;
import thaumicenergistics.common.tiles.TileInfusionMonitor;
import thaumicenergistics.common.tiles.TileInfusionProvider;
import thaumicenergistics.init.ModBlocks; import thaumicenergistics.init.ModBlocks;
@WailaPlugin @WailaPlugin
@@ -14,11 +18,29 @@ public class ThEJadePlugin implements IWailaPlugin {
@Override @Override
public void register(IWailaCommonRegistration registration) { public void register(IWailaCommonRegistration registration) {
registration.registerBlockDataProvider(VibrationChamberProvider.INSTANCE, TileEssentiaVibrationChamber.class); registration.registerBlockDataProvider(VibrationChamberProvider.INSTANCE, TileEssentiaVibrationChamber.class);
registration.registerBlockDataProvider(InfusionMonitorProvider.INSTANCE, TileInfusionMonitor.class);
// 注魔供应器 + 源质供应器共用网络状态显示
registration.registerBlockDataProvider(InfusionProviderProvider.INSTANCE, TileInfusionProvider.class);
registration.registerBlockDataProvider(InfusionProviderProvider.INSTANCE, TileEssentiaProvider.class);
// 奥术装配器:网络状态 + 要素源状态
registration.registerBlockDataProvider(AssemblerProvider.INSTANCE, TileArcaneAssembler.class);
} }
@Override @Override
public void registerClient(IWailaClientRegistration registration) { public void registerClient(IWailaClientRegistration registration) {
Block chamberBlock = ModBlocks.ESSENTIA_VIBRATION_CHAMBER.get(); Block chamberBlock = ModBlocks.ESSENTIA_VIBRATION_CHAMBER.get();
registration.registerBlockComponent(VibrationChamberProvider.INSTANCE, chamberBlock.getClass()); registration.registerBlockComponent(VibrationChamberProvider.INSTANCE, chamberBlock.getClass());
Block monitorBlock = ModBlocks.INFUSION_MONITOR.get();
registration.registerBlockComponent(InfusionMonitorProvider.INSTANCE, monitorBlock.getClass());
// 注魔供应器 + 源质供应器共用网络状态显示
Block infusionProviderBlock = ModBlocks.INFUSION_PROVIDER.get();
registration.registerBlockComponent(InfusionProviderProvider.INSTANCE, infusionProviderBlock.getClass());
Block essentiaProviderBlock = ModBlocks.ESSENTIA_PROVIDER.get();
registration.registerBlockComponent(InfusionProviderProvider.INSTANCE, essentiaProviderBlock.getClass());
Block assemblerBlock = ModBlocks.ARCANE_ASSEMBLER.get();
registration.registerBlockComponent(AssemblerProvider.INSTANCE, assemblerBlock.getClass());
} }
} }
@@ -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());
} }
} }
} }
@@ -9,7 +9,6 @@ import thaumcraft.api.research.ResearchCategories;
import thaumcraft.api.research.ResearchItem; import thaumcraft.api.research.ResearchItem;
import thaumcraft.api.research.ResearchPage; import thaumcraft.api.research.ResearchPage;
import thaumcraft.common.research.TCResearchIndex; import thaumcraft.common.research.TCResearchIndex;
import thaumicenergistics.ThaumicEnergistics;
/** /**
* 伪前置桩节点。在 ThE 的研究 tab 中创建一个占位节点, * 伪前置桩节点。在 ThE 的研究 tab 中创建一个占位节点,
@@ -94,11 +93,6 @@ public class PseudoResearchItem extends ResearchItem {
// 保存真实研究引用:内容(名称/文字/页面)通过方法委托到真实研究(TC port 渲染调 getPages()) // 保存真实研究引用:内容(名称/文字/页面)通过方法委托到真实研究(TC port 渲染调 getPages())
pseudo.realResearch = realResearch; pseudo.realResearch = realResearch;
ThaumicEnergistics.LOG.debug("[Pseudo] {} ← {}:{} icon={}",
pseudoKey, realCategory, realKey,
pseudo.icon_item != null && !pseudo.icon_item.isEmpty()
? pseudo.icon_item.getDescriptionId() : String.valueOf(pseudo.icon_resource));
return pseudo; return pseudo;
} }
@@ -26,42 +26,76 @@ public final class TCReflection {
private TCReflection() {} private TCReflection() {}
/** 从荒古炼金炉内部源质缓存直接抽取(替代蒸馏釜/罐子中转)。 */ /** 读取荒古炼金炉 private aspects 字段(不实现 IAspectContainer;只读,返回活引用用于原地修改)。 */
@Nullable @Nullable
private static AspectStack extractFromFurnace(BlockEntity be, int maxAmount) { private static AspectList getFurnaceAspects(BlockEntity be) {
try { try {
if (furnaceAspectsField == null) { if (furnaceAspectsField == null) {
furnaceAspectsField = be.getClass().getDeclaredField("aspects"); furnaceAspectsField = be.getClass().getDeclaredField("aspects");
furnaceAspectsField.setAccessible(true); furnaceAspectsField.setAccessible(true);
} }
AspectList furnaceAspects = (AspectList) furnaceAspectsField.get(be); Object raw = furnaceAspectsField.get(be);
if (furnaceAspects != null && furnaceAspects.size() > 0) { return raw instanceof AspectList list ? list : null;
Aspect[] aspects = furnaceAspects.getAspects(); } catch (Exception e) {
for (Aspect aspect : aspects) { return null;
}
}
/** 荒古炼金炉写回(aspects 为 private 字段,无公开写入 API)。返回实际写入量。 */
private static int addToFurnace(BlockEntity be, Aspect aspect, int amount) {
if (amount <= 0) return 0;
try {
AspectList list = getFurnaceAspects(be);
if (list == null) {
if (furnaceAspectsField == null) return 0;
list = new AspectList();
furnaceAspectsField.set(be, list);
}
list.add(aspect, amount);
be.setChanged();
return amount;
} catch (Exception e) {
return 0;
}
}
/** 从荒古炼金炉内部源质缓存直接抽取(替代蒸馏釜/罐子中转)。only 非空时只抽指定方面。 */
@Nullable
private static AspectStack extractFromFurnace(BlockEntity be, @Nullable ResourceLocation only, int maxAmount) {
AspectList furnaceAspects = getFurnaceAspects(be);
if (furnaceAspects == null || furnaceAspects.size() <= 0) return null;
for (Aspect aspect : furnaceAspects.getAspects()) {
if (aspect == null) continue; if (aspect == null) continue;
int amt = furnaceAspects.getAmount(aspect); int amt = furnaceAspects.getAmount(aspect);
if (amt <= 0) continue; if (amt <= 0) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int toExtract = Math.min(amt, maxAmount); int toExtract = Math.min(amt, maxAmount);
furnaceAspects.remove(aspect, toExtract); furnaceAspects.remove(aspect, toExtract);
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, toExtract);
return id != null ? new AspectStack(id, toExtract) : null;
}
}
} catch (Exception ignored) {
} }
return null; return null;
} }
@Nullable @Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, int maxAmount) { public static AspectStack extractEssentia(@Nullable BlockEntity be, int maxAmount) {
return extractEssentia(be, null, maxAmount);
}
/**
* 从容器抽取源质。only 非空时只抽指定方面(存储总线按 AE 请求的 key 定向抽取)。
*/
@Nullable
public static AspectStack extractEssentia(@Nullable BlockEntity be, @Nullable ResourceLocation only, int maxAmount) {
if (be == null) { if (be == null) {
return null; return null;
} }
// ===== 路径0: 荒古炼金炉直连(AlchemicalFurnaceBlockEntity 不实现 IAspectContainer, // ===== 路径0: 荒古炼金炉直连(AlchemicalFurnaceBlockEntity 不实现 IAspectContainer,
// 源质在 private aspects 缓存——直接抽取,替代"必须经蒸馏釜/罐子")===== // 源质在 private aspects 缓存——直接抽取,替代"必须经蒸馏釜/罐子")=====
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) { if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
AspectStack fromFurnace = extractFromFurnace(be, maxAmount); AspectStack fromFurnace = extractFromFurnace(be, only, maxAmount);
if (fromFurnace != null) return fromFurnace; if (fromFurnace != null) return fromFurnace;
} }
// ===== 路径0b: TC 管道接口(EssentiaTransport——高级荒古炼金炉喷嘴/源质管道)===== // ===== 路径0b: TC 管道接口(EssentiaTransport——高级荒古炼金炉喷嘴/源质管道)=====
@@ -70,14 +104,16 @@ public final class TCReflection {
if (!transport.canOutputTo(face)) continue; if (!transport.canOutputTo(face)) continue;
Aspect aspect = transport.getEssentiaType(face); Aspect aspect = transport.getEssentiaType(face);
if (aspect == null) continue; if (aspect == null) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int amount = transport.getEssentiaAmount(face); int amount = transport.getEssentiaAmount(face);
if (amount <= 0) continue; if (amount <= 0) continue;
int toExtract = Math.min(amount, maxAmount); int toExtract = Math.min(amount, maxAmount);
int taken = transport.takeEssentia(aspect, toExtract, face); int taken = transport.takeEssentia(aspect, toExtract, face);
if (taken > 0) { if (taken > 0) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, taken);
return id != null ? new AspectStack(id, taken) : null;
} }
} }
} }
@@ -94,11 +130,13 @@ public final class TCReflection {
if (aspect == null) continue; if (aspect == null) continue;
int amt = aspectList.getAmount(aspect); int amt = aspectList.getAmount(aspect);
if (amt <= 0) continue; if (amt <= 0) continue;
ResourceLocation id = getAspectId(aspect);
if (id == null) continue;
if (only != null && !only.equals(id)) continue;
int toExtract = Math.min(amt, maxAmount); int toExtract = Math.min(amt, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) { if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, toExtract);
return id != null ? new AspectStack(id, toExtract) : null;
} }
} }
} }
@@ -113,11 +151,14 @@ public final class TCReflection {
Aspect aspect = (Aspect) methods[0].invoke(be); Aspect aspect = (Aspect) methods[0].invoke(be);
int amount = (int) methods[1].invoke(be); int amount = (int) methods[1].invoke(be);
if (aspect != null && amount > 0) { if (aspect != null && amount > 0) {
ResourceLocation id = getAspectId(aspect);
if (id == null || (only != null && !only.equals(id))) {
return null;
}
int toExtract = Math.min(amount, maxAmount); int toExtract = Math.min(amount, maxAmount);
if (container.takeFromContainer(aspect, toExtract)) { if (container.takeFromContainer(aspect, toExtract)) {
be.setChanged(); be.setChanged();
ResourceLocation id = getAspectId(aspect); return new AspectStack(id, toExtract);
return id != null ? new AspectStack(id, toExtract) : null;
} }
} }
} catch (Exception ignored) { } catch (Exception ignored) {
@@ -128,19 +169,28 @@ public final class TCReflection {
public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) { public static void returnEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return; if (be == null || stack.isEmpty()) return;
if (!(be instanceof IAspectContainer container)) return;
Aspect aspect = findAspectById(stack.aspectId()); Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return; if (aspect == null) return;
// 荒古炼金炉:不实现 IAspectContainer,源质在 private aspects 字段 → 反射写回。
// (此前直接 return 导致导入总线"网络已满"时抽出的源质无处归还而消失)
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
addToFurnace(be, aspect, (int) stack.amount());
return;
}
if (!(be instanceof IAspectContainer container)) return;
container.addToContainer(aspect, (int) stack.amount()); container.addToContainer(aspect, (int) stack.amount());
be.setChanged(); be.setChanged();
} }
public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) { public static int insertEssentia(@Nullable BlockEntity be, AspectStack stack) {
if (be == null || stack.isEmpty()) return 0; if (be == null || stack.isEmpty()) return 0;
if (!(be instanceof IAspectContainer container)) return 0;
Aspect aspect = findAspectById(stack.aspectId()); Aspect aspect = findAspectById(stack.aspectId());
if (aspect == null) return 0; if (aspect == null) return 0;
int toInsert = (int) stack.amount(); int toInsert = (int) Math.min(stack.amount(), Integer.MAX_VALUE);
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
return addToFurnace(be, aspect, toInsert);
}
if (!(be instanceof IAspectContainer container)) return 0;
int leftover = container.addToContainer(aspect, toInsert); int leftover = container.addToContainer(aspect, toInsert);
int added = toInsert - leftover; int added = toInsert - leftover;
if (added > 0) { if (added > 0) {
@@ -149,6 +199,61 @@ public final class TCReflection {
return added; return added;
} }
/** 非破坏性列出容器内全部源质(存储总线挂载为网络存储、导入总线预检用)。 */
public static java.util.List<AspectStack> listEssentia(@Nullable BlockEntity be) {
java.util.List<AspectStack> result = new java.util.ArrayList<>();
if (be == null) return result;
// 荒古炼金炉(不实现 IAspectContainer)
if (FURNACE_CLASS_NAME.equals(be.getClass().getName())) {
AspectList furnaceAspects = getFurnaceAspects(be);
if (furnaceAspects != null) {
for (Aspect aspect : furnaceAspects.getAspects()) {
if (aspect == null) continue;
int amt = furnaceAspects.getAmount(aspect);
ResourceLocation id = getAspectId(aspect);
if (amt > 0 && id != null) result.add(new AspectStack(id, amt));
}
}
return result;
}
// TC 管道接口
if (be instanceof EssentiaTransport transport) {
for (Direction face : Direction.values()) {
if (!transport.canOutputTo(face)) continue;
Aspect aspect = transport.getEssentiaType(face);
if (aspect == null) continue;
int amount = transport.getEssentiaAmount(face);
ResourceLocation id = getAspectId(aspect);
if (amount > 0 && id != null) result.add(new AspectStack(id, amount));
}
return result;
}
// IAspectContainer(罐子/奥术蒸馏器等)
if (be instanceof IAspectContainer container) {
AspectList list = container.getAspects();
if (list != null) {
for (Aspect aspect : list.getAspects()) {
if (aspect == null) continue;
int amt = list.getAmount(aspect);
ResourceLocation id = getAspectId(aspect);
if (amt > 0 && id != null) result.add(new AspectStack(id, amt));
}
}
}
return result;
}
/** 非破坏性窥视第一个可抽取的源质(导入总线预检:先看容器能给什么,再决定抽多少)。 */
@Nullable
public static AspectStack peekEssentia(@Nullable BlockEntity be, int maxAmount) {
for (AspectStack stack : listEssentia(be)) {
if (stack.isEmpty()) continue;
long amt = Math.min(stack.amount(), Math.max(0, maxAmount));
if (amt > 0) return new AspectStack(stack.aspectId(), amt);
}
return null;
}
@Nullable @Nullable
public static Aspect findAspectById(ResourceLocation id) { public static Aspect findAspectById(ResourceLocation id) {
if (id == null) return null; if (id == null) return null;
@@ -0,0 +1,64 @@
package thaumicenergistics.common.integration.tc;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import thaumcraft.api.aspects.IEssentiaContainerItem;
import thaumcraft.common.items.AspectEssenceItem;
import thaumcraft.common.items.JarBlockItem;
/**
* 源质容器判定:只有要素安瓿与要素罐子才算"源质容器",参与右键存入/装罐。
* <p>
* 不能只判 {@code IEssentiaContainerItem}:TC 里天域之华(wisp_essence)、源质水晶(CRYSTAL)
* 等物品也实现了该接口,但它们不是容器——若不排除,右键会被当源质抽走(例如天域之华
* 会被整个抽干变成空玻璃安瓿)。同一实现类 {@link AspectEssenceItem} 用
* {@link AspectEssenceItem#kind()} 区分形态:
* <ul>
* <li>要素安瓿(变体/自定义要素):{@code AspectEssenceItem$Kind.PHIAL}(固定 8 源质)</li>
* <li>空玻璃安瓿:{@code thaumcraft:glass_phial}(普通 Item,无 Kind)</li>
* <li>要素罐子:{@link JarBlockItem}(TC 罐子统一类型,容量 64)</li>
* <li>天域之华 / 水晶:{@code Kind.WISP / Kind.CRYSTAL} —— 一律排除</li>
* </ul>
*/
public final class ThEEssentiaContainers {
private static final ResourceLocation GLASS_PHIAL_ID =
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial");
private ThEEssentiaContainers() {}
/** 要素安瓿:AspectEssenceItem(Kind.PHIAL),或空玻璃安瓿(glass_phial 无 Kind)。 */
public static boolean isPhial(ItemStack stack) {
if (stack.isEmpty()) {
return false;
}
if (stack.getItem() instanceof AspectEssenceItem essence) {
return essence.kind() == AspectEssenceItem.Kind.PHIAL;
}
return stack.getItem() == BuiltInRegistries.ITEM.get(GLASS_PHIAL_ID);
}
/** 要素罐子:TC 罐子统一类型(JarBlockItem)。 */
public static boolean isJar(ItemStack stack) {
return !stack.isEmpty() && stack.getItem() instanceof JarBlockItem;
}
/** 是否源质容器(安瓿或罐子)——右键存入/装罐的唯一判定。 */
public static boolean isEssentiaContainer(ItemStack stack) {
return isPhial(stack) || isJar(stack);
}
/** 容器是否装有源质。空玻璃安瓿恒为 false;天域之华/水晶等非容器恒为 false。 */
public static boolean hasEssentia(ItemStack stack) {
if (!isEssentiaContainer(stack)) {
return false;
}
if (stack.getItem() instanceof IEssentiaContainerItem container) {
var aspects = container.getAspects(stack);
return aspects != null && !aspects.isEmpty();
}
return false;
}
}
@@ -4,7 +4,6 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import thaumcraft.api.ThaumcraftApi;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList; import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.events.ResearchCompletedEvent; import thaumcraft.api.events.ResearchCompletedEvent;
@@ -60,8 +59,6 @@ public final class ThEThaumcraft {
ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/tab_icon.png"), ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/tab_icon.png"),
ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/research_background.png")); ResourceLocation.fromNamespaceAndPath(MODID, "textures/research/research_background.png"));
registerScanEvent();
// ⚠ 必须先在当前 tab 注册伪前置节点(替代 TC 原版研究), // ⚠ 必须先在当前 tab 注册伪前置节点(替代 TC 原版研究),
// 否则 setParents 跨分类引用时线画不出来。 // 否则 setParents 跨分类引用时线画不出来。
registerPseudoParents(); registerPseudoParents();
@@ -69,45 +66,26 @@ public final class ThEThaumcraft {
// 注册事件监听:当 TC 原版研究解锁时,自动 complete 对应伪前置节点 // 注册事件监听:当 TC 原版研究解锁时,自动 complete 对应伪前置节点
registerPseudoSync(); registerPseudoSync();
// 严格按照 1.7.10 FeatureRegistry 构造函数中的注册顺序 // 注册顺序沿袭 1.7.10 移植(决定研究 tab 内节点坐标布局),勿随意调整。
// 1. FeatureThaumicEnergistics → BASEENERGISTICS (0,0)
registerBaseEnergistics(); registerBaseEnergistics();
// 2. FeatureAutocrafting_Arcane → ARCANE_ASSEMBLER + KNOWLEDGE_INSCRIBER
registerArcaneAssembler(); registerArcaneAssembler();
registerKnowledgeInscriber(); registerKnowledgeInscriber();
// 3. FeatureCells → ESSENTIASTORAGE (0,2) 向左延伸 1k/4k/16k/64k
registerEssentiaStorage(); registerEssentiaStorage();
// 4. FeatureACT → ARCANECRAFTINGTERMINAL (2,-1)
registerArcaneCraftingTerminal(); registerArcaneCraftingTerminal();
// 5. FeatureVisRelayInterface → VISINTERFACE (4,-1)
registerVisInterface(); registerVisInterface();
// 6. FeatureEssentiaIOBuses → ESSENTIABUSES (-2,-2)
registerEssentiaBuses(); registerEssentiaBuses();
// 7. FeatureInfusionProvider → INFUSIONPROVIDER (-5,-2)
registerInfusionProvider(); registerInfusionProvider();
// 8. FeatureEssentiaProvider → ESSENTIAPROVIDER (-2,-4) registerInfusionMonitor();
registerEssentiaProvider(); registerEssentiaProvider();
// 9. FeatureEssentiaMonitoring → ESSENTIATERMINAL (-1,-4)
registerEssentiaTerminal(); registerEssentiaTerminal();
// 10. FeatureConversionCores → DIGISENTIA (0,-2)
registerDigisentia(); registerDigisentia();
// 12. FeatureWrenchFocus → FOCUS_AEWRENCH (-3,-7)
registerFocusAEWrench(); registerFocusAEWrench();
// 13. FeatureQuartzDupe → CERTUS_DUPE (-5,-5)
registerCertusDupe(); registerCertusDupe();
// 14. FeatureEssentiaVibrationChamber → VIBRATIONCHAMBER (1,-4)
registerEssentiaVibrationChamber(); registerEssentiaVibrationChamber();
// 15. FeatureAutocrafting_Essentia → DISTILLATIONENCODER (-4,-4)
registerDistillationEncoder(); registerDistillationEncoder();
// 16. FeatureGolemBackpack → GOLEMWIFIBACKPACK (0,-6)
registerGolemWifiBackpack(); registerGolemWifiBackpack();
LOG.info("ThaumicEnergistics research registered ({} nodes).", 13); LOG.info("ThaumicEnergistics research registered.");
}
private static void registerScanEvent() {
ThaumcraftApi.registerScanEventhandler((scanner, level, player) ->
java.util.Optional.empty());
} }
// ==================== 伪前置节点(跨分类引用 TC 原版研究) ==================== // ==================== 伪前置节点(跨分类引用 TC 原版研究) ====================
@@ -162,8 +140,7 @@ public final class ThEThaumcraft {
// 自动 complete 伪前置节点(不通知玩家) // 自动 complete 伪前置节点(不通知玩家)
try { try {
TCResearchManager.complete(rc.player(), pseudoKey, false); TCResearchManager.complete(rc.player(), pseudoKey, false);
} catch (Exception e) { } catch (Exception ignored) {
LOG.debug("伪前置自动解锁失败: {} → {}", completedKey, pseudoKey);
} }
}); });
} }
@@ -282,6 +259,20 @@ public final class ThEThaumcraft {
.registerResearchItem(); .registerResearchItem();
} }
// ==================== 8. (-7,-2) INFUSIONMONITOR 韵魔观测器====================
// 前置: INFUSIONPROVIDER(注魔供应器);位于其左侧。无需研究比较,直接花费要素解锁。
private static void registerInfusionMonitor() {
new ResearchItem("INFUSIONMONITOR", TAB,
new AspectList().add(Aspect.MAGIC, 4).add(Aspect.AURA, 4).add(Aspect.SENSES, 6),
-7, -2, 1, new ItemStack(ModItems.INFUSION_MONITOR_ITEM.get()))
.setRound().setParents("INFUSIONPROVIDER")
.setPages(
new ResearchPage("tc.research_text.INFUSIONMONITOR.stage.1"),
new ResearchPage(RecipeRegistration.INFUSION_MONITOR),
new ResearchPage("tc.research_text.INFUSIONMONITOR.stage.2"))
.registerResearchItem();
}
// ==================== 8. (-2,-4) ESSENTIAPROVIDER 源质供应器==================== // ==================== 8. (-2,-4) ESSENTIAPROVIDER 源质供应器====================
// 前置: ESSENTIABUSES(源质总线) // 前置: ESSENTIABUSES(源质总线)
private static void registerEssentiaProvider() { private static void registerEssentiaProvider() {
@@ -290,9 +281,15 @@ public final class ThEThaumcraft {
-2, -4, 3, new ItemStack(ModItems.ESSENTIA_PROVIDER_ITEM.get())) -2, -4, 3, new ItemStack(ModItems.ESSENTIA_PROVIDER_ITEM.get()))
.setRound().setParents("ESSENTIABUSES") .setRound().setParents("ESSENTIABUSES")
.setPages( .setPages(
// 源质供应器:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.1"), new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER), new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER),
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.stage.2")) // 源质无线接收器:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.connection.stage.1"),
new ResearchPage(RecipeRegistration.ESSENTIA_PROVIDER_CONNECTION),
// 源质无线绑定工具:介绍+用法 / 配方
new ResearchPage("tc.research_text.ESSENTIAPROVIDER.tool.stage.1"),
new ResearchPage(RecipeRegistration.WIRELESS_CONNECTOR))
.registerResearchItem(); .registerResearchItem();
} }
@@ -6,6 +6,9 @@ import java.util.Set;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.tooltip.TooltipComponent; import net.minecraft.world.inventory.tooltip.TooltipComponent;
import net.minecraft.world.item.Item; import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
@@ -21,10 +24,12 @@ import appeng.api.upgrades.UpgradeInventories;
import appeng.items.contents.CellConfig; import appeng.items.contents.CellConfig;
import appeng.me.cells.BasicCellInventory; import appeng.me.cells.BasicCellInventory;
import appeng.util.ConfigInventory; import appeng.util.ConfigInventory;
import net.minecraft.world.level.Level;
import thaumicenergistics.common.integration.appeng.AEssentiaKey; import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType; import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.config.ThEConfig; import thaumicenergistics.config.ThEConfig;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import thaumicenergistics.init.ModItems;
import java.util.List; import java.util.List;
@@ -207,6 +212,38 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
} }
} }
//shift+右键 返回 对应等级存储组件+源质存储外壳
@Override
public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) {
ItemStack held = player.getItemInHand(hand);
if (!level.isClientSide) {
if (player.isShiftKeyDown()) {
if (held.getItem() instanceof ItemEssentiaCell cellItem) {
String tier = cellItem.getTier();
Item componentItem = switch (tier) {
case "1k" -> ModItems.STORAGE_COMPONENT_1K.get();
case "4k" -> ModItems.STORAGE_COMPONENT_4K.get();
case "16k" -> ModItems.STORAGE_COMPONENT_16K.get();
case "64k" -> ModItems.STORAGE_COMPONENT_64K.get();
default -> null;
};
if(componentItem != null){
player.addItem(new ItemStack(componentItem,1));
player.addItem(new ItemStack(ModItems.STORAGE_CASING.get(),1));
}
}
}
}
if(player.isShiftKeyDown()){
return InteractionResultHolder.success(ItemStack.EMPTY);
}else{
return InteractionResultHolder.pass(held);
}
}
// ===== 工厂方法 ===== // ===== 工厂方法 =====
public static ItemEssentiaCell create1k(Item.Properties props) { public static ItemEssentiaCell create1k(Item.Properties props) {
return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5); return new ItemEssentiaCell(props, "1k", 1, 8, ThEConfig.CELL_MAX_TYPES, 0.5);
@@ -0,0 +1,161 @@
package thaumicenergistics.common.items;
import net.minecraft.core.BlockPos;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import thaumicenergistics.common.tiles.TileEssentiaProvider;
import thaumicenergistics.common.tiles.TileEssentiaProviderConnection;
import thaumicenergistics.init.ModBlocks;
/**
* 源质无线绑定工具:
* - Shift+右键:绑定/断开(未连接的接收器=选中;供应器=绑定选中的接收器;已连接的接收器=断开)
* - 右键:显示连接信息(聊天栏,坐标可复制)
*/
public class WirelessConnectorItem extends Item {
private static final String KEY_SELECTED_RECEIVER = "SelectedReceiver";
private static final String KEY_SELECTED_PROVIDER = "SelectedProvider";
public WirelessConnectorItem(Properties properties) {
super(properties);
}
private static CompoundTag getSelectionTag(ItemStack stack) {
return stack.getOrDefault(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.EMPTY).copyTag();
}
private static void setSelectionTag(ItemStack stack, CompoundTag tag) {
stack.set(DataComponents.CUSTOM_DATA, net.minecraft.world.item.component.CustomData.of(tag));
}
private static void clearSelection(ItemStack stack) {
stack.remove(DataComponents.CUSTOM_DATA);
}
private static void showReceiverInfo(Player player, TileEssentiaProviderConnection receiver) {
BlockPos lp = receiver.getLinkedProviderPos();
if (lp != null) {
player.sendSystemMessage(Component.literal("§7[源质无线接收器] 已连接供应器: §a[" + lp.getX() + ", " + lp.getY() + ", " + lp.getZ() + "]"));
} else {
player.sendSystemMessage(Component.literal("§7[源质无线接收器] 未连接任何供应器"));
}
}
private static void showProviderInfo(Player player, TileEssentiaProvider provider) {
var list = provider.getLinkedReceivers();
player.sendSystemMessage(Component.literal("§7[源质供应器] 已连接接收器 (" + list.size() + "/" + TileEssentiaProvider.MAX_LINKED_RECEIVERS + "):"));
if (list.isEmpty()) {
player.sendSystemMessage(Component.literal("§7 (无)"));
} else {
for (BlockPos rp : list) {
player.sendSystemMessage(Component.literal("§a [" + rp.getX() + ", " + rp.getY() + ", " + rp.getZ() + "]"));
}
}
}
@Override
public InteractionResult useOn(UseOnContext context) {
Level level = context.getLevel();
BlockPos pos = context.getClickedPos();
Player player = context.getPlayer();
ItemStack stack = context.getItemInHand();
if (level.isClientSide() || player == null) {
return InteractionResult.SUCCESS;
}
BlockState state = level.getBlockState(pos);
BlockEntity be = level.getBlockEntity(pos);
// ===== Shift+右键:绑定 / 断开(顺序不限,第二步完成绑定) =====
if (player.isShiftKeyDown()) {
if (be instanceof TileEssentiaProviderConnection receiver) {
if (receiver.getLinkedProviderPos() != null) {
// 已连接 → 断开
receiver.clearLink();
player.sendSystemMessage(Component.literal("§e已断开该源质无线接收器的连接"));
return InteractionResult.SUCCESS;
}
CompoundTag tag = getSelectionTag(stack);
if (tag.contains(KEY_SELECTED_PROVIDER)) {
// 已选中供应器 → 绑定到它
BlockPos providerPos = BlockPos.of(tag.getLong(KEY_SELECTED_PROVIDER));
String error = receiver.setLinkedProvider(providerPos);
if (error == null) {
player.sendSystemMessage(Component.literal("§a绑定成功!无线接收器已连接至源质供应器"));
} else {
player.sendSystemMessage(Component.literal(error));
}
clearSelection(stack);
} else if (tag.contains(KEY_SELECTED_RECEIVER)) {
player.sendSystemMessage(Component.literal("§c接收器不能互相绑定,只能绑定到源质供应器"));
} else {
// 未选中 → 选中该接收器
tag.putLong(KEY_SELECTED_RECEIVER, pos.asLong());
setSelectionTag(stack, tag);
player.sendSystemMessage(Component.literal("§a已选中源质无线接收器,请 shift+右键 源质供应器完成绑定"));
}
return InteractionResult.SUCCESS;
}
if (be instanceof TileEssentiaProvider) {
CompoundTag tag = getSelectionTag(stack);
if (tag.contains(KEY_SELECTED_RECEIVER)) {
// 已选中接收器 → 绑定到本供应器
BlockPos receiverPos = BlockPos.of(tag.getLong(KEY_SELECTED_RECEIVER));
if (level.getBlockEntity(receiverPos) instanceof TileEssentiaProviderConnection receiver) {
String error = receiver.setLinkedProvider(pos);
if (error == null) {
player.sendSystemMessage(Component.literal("§a绑定成功!无线接收器已连接至源质供应器"));
} else {
player.sendSystemMessage(Component.literal(error));
}
} else {
player.sendSystemMessage(Component.literal("§c选中的源质无线接收器不存在或已被移除"));
}
clearSelection(stack);
} else {
// 未选中 → 选中该供应器
tag.putLong(KEY_SELECTED_PROVIDER, pos.asLong());
setSelectionTag(stack, tag);
player.sendSystemMessage(Component.literal("§a已选中源质供应器,请 shift+右键 源质无线接收器完成绑定"));
}
return InteractionResult.SUCCESS;
}
// 其他方块:清除选中
if (getSelectionTag(stack).contains(KEY_SELECTED_RECEIVER)
|| getSelectionTag(stack).contains(KEY_SELECTED_PROVIDER)) {
clearSelection(stack);
player.sendSystemMessage(Component.literal("§7已清除选中"));
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
// ===== 右键:显示连接信息(复制显示) =====
if (be instanceof TileEssentiaProviderConnection receiver) {
showReceiverInfo(player, receiver);
return InteractionResult.SUCCESS;
}
if (be instanceof TileEssentiaProvider provider) {
showProviderInfo(player, provider);
return InteractionResult.SUCCESS;
}
// 其他方块:清除选中
if (getSelectionTag(stack).contains(KEY_SELECTED_RECEIVER)
|| getSelectionTag(stack).contains(KEY_SELECTED_PROVIDER)) {
clearSelection(stack);
player.sendSystemMessage(Component.literal("§7已清除选中"));
return InteractionResult.SUCCESS;
}
return InteractionResult.PASS;
}
}
@@ -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,54 @@
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;
import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal;
/**
* 客户端→服务端 网络包:奥术合成终端清空合成网格 / 上报"关闭时自动清理"开关。
* <ul>
* <li>{@link #ACTION_STASH}:立即把网格物品存入 ME 网络(S_STASH 按钮 / 关闭终端时触发),
* 服务端直接执行,不依赖 AE2 quickMove/MOVE_REGION 语义链。</li>
* <li>{@link #ACTION_SET_CLEAR_ON_CLOSE}:上报客户端 AE2 设置"关闭时自动清理合成槽"的开关值,
* 服务端存到菜单实例上,菜单关闭(removed)时据此清理——不依赖 GUI onClose 是否触发。</li>
* </ul>
*/
public record ArcaneTerminalClearC2SPacket(int containerId, int action, boolean flag) implements CustomPacketPayload {
public static final int ACTION_STASH = 0;
public static final int ACTION_SET_CLEAR_ON_CLOSE = 1;
public static final Type<ArcaneTerminalClearC2SPacket> TYPE =
new Type<>(ThaumicEnergistics.id("arcane_terminal_clear"));
public static final StreamCodec<RegistryFriendlyByteBuf, ArcaneTerminalClearC2SPacket> STREAM_CODEC =
StreamCodec.composite(
ByteBufCodecs.VAR_INT, ArcaneTerminalClearC2SPacket::containerId,
ByteBufCodecs.VAR_INT, ArcaneTerminalClearC2SPacket::action,
ByteBufCodecs.BOOL, ArcaneTerminalClearC2SPacket::flag,
ArcaneTerminalClearC2SPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnServer(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (ctx.player().containerMenu.containerId == this.containerId
&& ctx.player().containerMenu instanceof ContainerArcaneCraftingTerminal menu) {
switch (this.action) {
case ACTION_STASH -> menu.clearGridToNetworkOnServer();
case ACTION_SET_CLEAR_ON_CLOSE -> menu.setClearOnClose(this.flag);
default -> {
}
}
}
});
}
}
@@ -0,0 +1,81 @@
package thaumicenergistics.common.network;
import java.util.List;
import net.minecraft.core.BlockPos;
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.server.level.ServerLevel;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.container.ContainerArcaneAssembler;
import thaumicenergistics.common.tiles.TileArcaneAssembler;
/**
* S2C:奥术装配器配方预览同步(3×3 材料 + 预产物)。
* 合成开始(active=true)发送材料与产物,结束/清空(active=false)。
* 直接发给打开该装配器菜单的玩家,不依赖 BE update tag(与容器数据同步同级可靠)。
*/
public record AssemblerPreviewSyncPacket(
BlockPos pos,
List<ItemStack> grid,
ItemStack result,
boolean active
) implements CustomPacketPayload {
public static final Type<AssemblerPreviewSyncPacket> TYPE =
new Type<>(ThaumicEnergistics.id("assembler_preview_sync"));
public static final StreamCodec<RegistryFriendlyByteBuf, AssemblerPreviewSyncPacket> STREAM_CODEC =
StreamCodec.composite(
BlockPos.STREAM_CODEC, AssemblerPreviewSyncPacket::pos,
ItemStack.OPTIONAL_STREAM_CODEC.apply(ByteBufCodecs.list(9)), AssemblerPreviewSyncPacket::grid,
ItemStack.OPTIONAL_STREAM_CODEC, AssemblerPreviewSyncPacket::result,
ByteBufCodecs.BOOL, AssemblerPreviewSyncPacket::active,
AssemblerPreviewSyncPacket::new
);
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
public void handleOnClient(IPayloadContext ctx) {
ctx.enqueueWork(() -> {
var player = ctx.player();
if (player != null && player.containerMenu instanceof ContainerArcaneAssembler menu
&& menu.assembler != null && menu.assembler.getBlockPos().equals(this.pos)) {
menu.applyClientPreview(this.grid, this.result, this.active);
}
});
}
/** 发给打开该装配器菜单的所有玩家。 */
public static void send(TileArcaneAssembler assembler, boolean active) {
if (assembler == null || assembler.getLevel() == null
|| !(assembler.getLevel() instanceof ServerLevel serverLevel)) {
return;
}
// null → EMPTY 兜底:previewGrid 任何时刻都不应含 null(序列化 ItemStack 会 NPE)
ItemStack[] grid = assembler.getPreviewGrid();
List<ItemStack> gridList = new java.util.ArrayList<>(9);
for (int i = 0; i < 9; i++) {
gridList.add(grid != null && grid[i] != null ? grid[i] : ItemStack.EMPTY);
}
ItemStack result = assembler.getCurrentResult();
var packet = new AssemblerPreviewSyncPacket(
assembler.getBlockPos(),
gridList,
result != null ? result : ItemStack.EMPTY,
active);
for (var player : serverLevel.players()) {
if (player.containerMenu instanceof ContainerArcaneAssembler menu && menu.assembler == assembler) {
PacketDistributor.sendToPlayer(player, packet);
}
}
}
}
@@ -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,63 @@
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;
// ⚠ 必须用玩家真实栈(carried/主手)而非包里的拷贝:装罐会原地消耗空安瓿(shrink)
// 或修改罐子内容(setAspects)。若用 sourceStack(拷贝),消耗/修改不落真实物品——
// 空安瓿不消耗却能凭空得到满安瓿。
// 顺序与客户端一致:优先 carried(拾取),其次主手;sourceStack 仅兜底。
var source = !menu.getCarried().isEmpty() ? menu.getCarried()
: (!player.getMainHandItem().isEmpty() ? player.getMainHandItem()
: this.sourceStack);
thaumicenergistics.common.container.EssentiaFillHelper.fillContainerFromGrid(
acc.thE_getStorage(), acc.thE_getEnergySource(),
menu.getActionSource(), player, source, this.aspectId);
menu.broadcastChanges();
}
});
}
}
@@ -50,11 +50,39 @@ 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,
ResearchGateErrorPacket.STREAM_CODEC, ResearchGateErrorPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx) (pkt, ctx) -> pkt.handleOnClient(ctx)
); );
// ArcaneTerminalClearC2SPacket — C2S(奥术合成终端清空合成网格——S_STASH 按钮 / 关闭时自动清理)
registrar.playToServer(
ArcaneTerminalClearC2SPacket.TYPE,
ArcaneTerminalClearC2SPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnServer(ctx)
);
// AssemblerPreviewSyncPacket — S2C(奥术装配器配方预览:3×3 材料 + 预产物)
registrar.playToClient(
AssemblerPreviewSyncPacket.TYPE,
AssemblerPreviewSyncPacket.STREAM_CODEC,
(pkt, ctx) -> pkt.handleOnClient(ctx)
);
} }
} }
@@ -5,24 +5,38 @@ import java.util.List;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.ItemStack;
import net.minecraft.world.phys.Vec3; import net.minecraft.world.phys.Vec3;
import appeng.api.implementations.items.IMemoryCard;
import appeng.api.implementations.items.MemoryCardMessages;
import appeng.api.ids.AEComponents;
import appeng.api.inventories.InternalInventory; import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.api.networking.ticking.IGridTickable;
import appeng.api.networking.ticking.TickRateModulation;
import appeng.api.networking.ticking.TickingRequest;
import appeng.api.parts.IPartItem; import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel; import appeng.api.parts.IPartModel;
import appeng.core.AppEng; import appeng.core.AppEng;
import appeng.items.parts.PartModels; import appeng.items.parts.PartModels;
import appeng.me.service.P2PService;
import appeng.menu.locator.MenuLocators; import appeng.menu.locator.MenuLocators;
import appeng.menu.MenuOpener; import appeng.menu.MenuOpener;
import appeng.parts.PartModel; import appeng.parts.PartModel;
import appeng.parts.p2p.P2PTunnelPart;
import appeng.parts.reporting.AbstractTerminalPart; import appeng.parts.reporting.AbstractTerminalPart;
import appeng.util.inv.AppEngInternalInventory; import appeng.util.inv.AppEngInternalInventory;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.common.items.TCFunctionalItems;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
public class ArcaneCraftingTerminalPart extends AbstractTerminalPart { public class ArcaneCraftingTerminalPart extends AbstractTerminalPart implements IGridTickable {
public static final ResourceLocation INV_CRAFTING = AppEng.makeId("arcane_crafting_terminal_crafting"); public static final ResourceLocation INV_CRAFTING = AppEng.makeId("arcane_crafting_terminal_crafting");
public static final ResourceLocation INV_WAND = ThaumicEnergistics.id("arcane_crafting_terminal_wand"); public static final ResourceLocation INV_WAND = ThaumicEnergistics.id("arcane_crafting_terminal_wand");
@@ -42,12 +56,22 @@ public class ArcaneCraftingTerminalPart extends AbstractTerminalPart {
public static final int WAND_SLOT_INDEX = 0; public static final int WAND_SLOT_INDEX = 0;
/** 充能间隔:每 tick 抽取一次。与 TC 能量节点池子每 tick 重置的频率、原版充能中继一致;无需放大。 */
private static final int VIS_REPLENISH_INTERVAL = 1;
/** 每 tick 每要素最大充能 cV,与原版奥术工作台充能中继(MagicWorkbenchCharger)一致(5 cV/tick = 1 vis/s)。 */
private static final int MAX_CVIS_PER_TICK = 5;
private static final String NBT_KEY_VIS_SOURCE_FREQ = "visSourceFreq";
/** 内存卡设定的 vis 源 P2P 频率;0 = 未设置(不自动充能)。 */
private short visSourceFreq = 0;
private final AppEngInternalInventory craftingGrid = new AppEngInternalInventory(this, 9); private final AppEngInternalInventory craftingGrid = new AppEngInternalInventory(this, 9);
private final AppEngInternalInventory wandInv = new AppEngInternalInventory(this, 1); private final AppEngInternalInventory wandInv = new AppEngInternalInventory(this, 1);
public ArcaneCraftingTerminalPart(IPartItem<?> partItem) { public ArcaneCraftingTerminalPart(IPartItem<?> partItem) {
super(partItem); super(partItem);
this.getMainNode().setIdlePowerUsage(0.5); this.getMainNode().setIdlePowerUsage(0.5);
this.getMainNode().addService(IGridTickable.class, this);
} }
@Override @Override
@@ -63,6 +87,88 @@ public class ArcaneCraftingTerminalPart extends AbstractTerminalPart {
return true; return true;
} }
/**
* 内存卡右键终端:读取 Vis Interface 的 P2P 频率,设为法杖充能的 vis 源。
* 复刻奥术装配器 {@code TileArcaneAssembler.onMemoryCardActivate} 的行为:
* 卡上带 P2P 频率(从 vis_interface shift+右键复制来的)→ 设为 vis 源;
* 空白卡 → 清空;其它 → 交给 AE2 标准内存卡逻辑。
*/
@Override
public boolean onUseItemOn(ItemStack heldItem, Player player, InteractionHand hand, Vec3 pos) {
if (heldItem.getItem() instanceof IMemoryCard memoryCard) {
Short freq = heldItem.get(AEComponents.EXPORTED_P2P_FREQUENCY);
if (freq != null) {
this.visSourceFreq = freq;
getHost().markForSave();
memoryCard.notifyUser(player, MemoryCardMessages.SETTINGS_LOADED);
return true;
} else if (heldItem.get(AEComponents.EXPORTED_SETTINGS_SOURCE) == null) {
// 空白卡 → 清空 vis 源
if (this.visSourceFreq != 0) {
this.visSourceFreq = 0;
getHost().markForSave();
}
memoryCard.notifyUser(player, MemoryCardMessages.SETTINGS_CLEARED);
return true;
}
}
return super.onUseItemOn(heldItem, player, hand, pos);
}
// ===== 法杖自动充能(内存卡链接 Vis Interface 作为 vis 源) =====
@Override
public TickingRequest getTickingRequest(IGridNode node) {
return new TickingRequest(VIS_REPLENISH_INTERVAL, VIS_REPLENISH_INTERVAL, false);
}
@Override
public TickRateModulation tickingRequest(IGridNode node, int ticksSinceLastCall) {
if (visSourceFreq == 0) {
return TickRateModulation.SAME;
}
ItemStack wandStack = wandInv.getStackInSlot(0);
if (wandStack.isEmpty() || !(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) {
return TickRateModulation.SAME;
}
IGrid grid = node.getGrid();
if (grid == null) {
return TickRateModulation.SAME;
}
P2PService p2p = P2PService.get(grid);
if (p2p == null) {
return TickRateModulation.SAME;
}
P2PTunnelPart<?> input = p2p.getInput(visSourceFreq);
if (!(input instanceof VisInterfacePart visSource)) {
return TickRateModulation.SAME;
}
boolean changed = false;
int maxVis = wand.getMaxVis(wandStack);
for (Aspect primal : Aspect.getPrimalAspects()) {
int needed = maxVis - wand.getVis(wandStack, primal);
if (needed <= 0) {
continue;
}
ResourceLocation id = TCReflection.getAspectId(primal);
if (id == null) {
continue;
}
// 每 tick 申请真实量(与池子每 tick 重置对齐),充入真实量,vis 守恒
int got = visSource.consumeVis(id, Math.min(MAX_CVIS_PER_TICK, needed));
if (got > 0) {
wand.addRealVis(wandStack, primal, got, true);
changed = true;
}
}
if (changed) {
// 法杖 vis 存储在 ItemStack NBT,手动标记保存
getHost().markForSave();
}
return TickRateModulation.SAME;
}
@Override @Override
public MenuType<?> getMenuType(Player player) { public MenuType<?> getMenuType(Player player) {
return ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get(); return ModMenuTypes.ARCANE_CRAFTING_TERMINAL.get();
@@ -95,6 +201,7 @@ public class ArcaneCraftingTerminalPart extends AbstractTerminalPart {
super.readFromNBT(data, registries); super.readFromNBT(data, registries);
this.craftingGrid.readFromNBT(data, "craftingGrid", registries); this.craftingGrid.readFromNBT(data, "craftingGrid", registries);
this.wandInv.readFromNBT(data, "wandInv", registries); this.wandInv.readFromNBT(data, "wandInv", registries);
this.visSourceFreq = data.contains(NBT_KEY_VIS_SOURCE_FREQ) ? data.getShort(NBT_KEY_VIS_SOURCE_FREQ) : 0;
} }
@Override @Override
@@ -102,6 +209,9 @@ public class ArcaneCraftingTerminalPart extends AbstractTerminalPart {
super.writeToNBT(data, registries); super.writeToNBT(data, registries);
this.craftingGrid.writeToNBT(data, "craftingGrid", registries); this.craftingGrid.writeToNBT(data, "craftingGrid", registries);
this.wandInv.writeToNBT(data, "wandInv", registries); this.wandInv.writeToNBT(data, "wandInv", registries);
if (visSourceFreq != 0) {
data.putShort(NBT_KEY_VIS_SOURCE_FREQ, visSourceFreq);
}
} }
@Override @Override
@@ -0,0 +1,134 @@
package thaumicenergistics.common.parts;
import java.util.List;
import java.util.function.Supplier;
import net.minecraft.network.chat.Component;
import net.minecraft.world.level.block.entity.BlockEntity;
import appeng.api.config.Actionable;
import appeng.api.networking.security.IActionSource;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.KeyCounter;
import appeng.api.storage.MEStorage;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.common.essentia.AspectContainer;
import thaumcraft.common.essentia.AspectTank;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
/**
* 把相邻源质容器(要素罐子 / 奥术蒸馏器 / 荒古炼金炉 / 源质管道)包装成 {@link MEStorage},
* 供源质存储总线通过 {@code IStorageProvider.mountInventories} 挂载到网络。
* <p>
* 容器内容始终实时读取(不缓存),因此终端、子网、输入/输出总线看到的都是当前存量。
*/
public class EssentiaContainerStorage implements MEStorage {
/** 每次实时解析相邻方块实体(总线朝向方块可被替换/移除)。 */
private final Supplier<BlockEntity> target;
private final Component description;
public EssentiaContainerStorage(Supplier<BlockEntity> target, Component description) {
this.target = target;
this.description = description;
}
@Override
public long insert(AEKey what, long amount, Actionable mode, IActionSource source) {
if (amount <= 0 || !(what instanceof AEssentiaKey key)) {
return 0;
}
BlockEntity be = target.get();
if (be == null) {
return 0;
}
Aspect aspect = TCReflection.findAspectById(key.getId());
if (aspect == null) {
return 0;
}
// TC 内部 AspectContainer(要素罐子 / 奥术蒸馏器 等):单方面储罐,容量可精确读取,
// 因此 SIMULATE 能如实回答"还能装多少"。输入总线的预检依赖这一点:
// 旧实现一律乐观返回 amount,导致容器已满时仍判定"网络装得下",把源质从炉子里抽走。
if (be instanceof AspectContainer aspectContainer) {
AspectTank tank = aspectContainer.aspectTank();
if (tank == null) {
return 0;
}
int free;
if (tank.isEmpty()) {
free = tank.capacity();
} else if (tank.aspect() == aspect) {
free = tank.capacity() - tank.amount();
} else {
free = 0; // 单方面储罐里装的是别的方面
}
if (free <= 0) {
return 0;
}
int request = (int) Math.min(amount, free);
if (mode == Actionable.SIMULATE) {
return request;
}
int accepted = tank.insert(aspect, request);
if (accepted > 0) {
be.setChanged();
}
return accepted;
}
// 其他容器没有"模拟插入"API:SIMULATE 乐观返回,实际插入按容器返回值结算
// (输入总线侧有缓冲兜底,模拟与实插不一致时源质也不会丢)
if (mode == Actionable.SIMULATE) {
return amount;
}
return TCReflection.insertEssentia(be, new AspectStack(key.getId(), amount));
}
@Override
public long extract(AEKey what, long amount, Actionable mode, IActionSource source) {
if (amount <= 0 || !(what instanceof AEssentiaKey key)) {
return 0;
}
BlockEntity be = target.get();
if (be == null) {
return 0;
}
int request = (int) Math.min(amount, Integer.MAX_VALUE);
if (mode == Actionable.SIMULATE) {
// 只统计、不动容器
for (AspectStack stack : TCReflection.listEssentia(be)) {
if (stack.aspectId().equals(key.getId())) {
return Math.min(stack.amount(), request);
}
}
return 0;
}
AspectStack extracted = TCReflection.extractEssentia(be, key.getId(), request);
return extracted == null ? 0 : extracted.amount();
}
@Override
public void getAvailableStacks(KeyCounter out) {
BlockEntity be = target.get();
if (be == null) {
return;
}
List<AspectStack> stacks = TCReflection.listEssentia(be);
for (AspectStack stack : stacks) {
if (stack.amount() <= 0) continue;
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
if (key != null) {
out.add(key, stack.amount());
}
}
}
@Override
public Component getDescription() {
return description;
}
}
@@ -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
@@ -60,8 +60,27 @@ public class EssentiaImportBusPart 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 String NBT_BUFFER_ASPECT = "ThEBufferAspect";
private static final String NBT_BUFFER_AMOUNT = "ThEBufferAmount";
private boolean isWorkingFlag = false; private boolean isWorkingFlag = false;
/**
* 待注入 AE 网络的源质缓冲(随总线 NBT 持久化)。
* <p>
* 网络装不下(无源质存储 / 已满 / 模拟与实插不一致)时源质留在这里重试,
* 绝不丢弃;缓冲非空期间也不再从容器抽取,避免"吸走却存不下"。
*/
private AspectStack buffer = AspectStack.EMPTY;
/**
* 单次传输量 = 基础速率 × 每 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);
@@ -95,37 +114,72 @@ public class EssentiaImportBusPart extends IOBusPart
protected boolean doBusWork(IGrid grid) { protected boolean doBusWork(IGrid grid) {
this.isWorkingFlag = false; this.isWorkingFlag = false;
// 1) 先冲缓冲:网络有空间时把上次暂存的源质注入
boolean worked = flushBuffer(grid);
Direction side = this.getSide(); Direction side = this.getSide();
if (side == null) return false; if (side == null) return worked;
BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side); BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side);
Level levelRaw = this.getLevel(); Level levelRaw = this.getLevel();
if (!(levelRaw instanceof ServerLevel level)) return false; if (!(levelRaw instanceof ServerLevel level)) return worked;
if (!level.isLoaded(adjacentPos)) return false; if (!level.isLoaded(adjacentPos)) return worked;
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos); BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false; if (adjacentBE == null) return worked;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE); // 2) 缓冲仍有剩余 = 网络装不下 → 不再从容器抽取(源质留在原容器里)
if (extracted == null || extracted.isEmpty()) return false; if (!this.buffer.isEmpty()) return worked;
// 3) 预检 1:先窥视容器能给什么(非破坏性)。无源质直接退出。
int rate = getTransferRate();
AspectStack available = TCReflection.peekEssentia(adjacentBE, rate);
if (available == null || available.isEmpty()) return worked;
AEssentiaKey key = AEssentiaKey.of(available.aspectId());
if (key == null) return worked;
// 4) 预检 2:网络能接收多少。为 0(无源质存储/已满)则完全不抽取。
long acceptable = grid.getStorageService().getInventory().insert(
key, available.amount(), Actionable.SIMULATE, IActionSource.ofMachine(this));
if (acceptable <= 0) return worked;
int toExtract = (int) Math.min(available.amount(), acceptable);
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, available.aspectId(), toExtract);
if (extracted == null || extracted.isEmpty()) return worked;
long injected = this.injectToNetwork(grid, extracted); long injected = this.injectToNetwork(grid, extracted);
if (injected <= 0) { if (injected < extracted.amount()) {
TCReflection.returnEssentia(adjacentBE, extracted); // 模拟与实插不一致(例如挂载的容器存储无法模拟)→ 剩余留在缓冲里重试,绝不归还/丢弃
return false; this.buffer = new AspectStack(extracted.aspectId(), extracted.amount() - injected);
markForSave();
} }
double powerCost = injected * TRANSFER_POWER; double powerCost = injected * TRANSFER_POWER;
grid.getEnergyService().extractAEPower(powerCost, Actionable.MODULATE, PowerMultiplier.CONFIG); grid.getEnergyService().extractAEPower(powerCost, Actionable.MODULATE, PowerMultiplier.CONFIG);
if (injected < extracted.amount()) { this.isWorkingFlag = injected > 0 || worked;
long leftover = extracted.amount() - injected; return injected > 0 || worked;
TCReflection.returnEssentia(adjacentBE, new AspectStack(extracted.aspectId(), leftover));
} }
/** 把缓冲里的源质注入网络。@return 本次是否注入了任意数量 */
private boolean flushBuffer(IGrid grid) {
if (this.buffer.isEmpty()) return false;
long injected = this.injectToNetwork(grid, this.buffer);
if (injected <= 0) return false;
long left = this.buffer.amount() - injected;
this.buffer = left > 0 ? new AspectStack(this.buffer.aspectId(), left) : AspectStack.EMPTY;
markForSave();
this.isWorkingFlag = true; this.isWorkingFlag = true;
return true; return true;
} }
private void markForSave() {
if (getHost() != null) {
getHost().markForSave();
}
}
private long injectToNetwork(IGrid grid, AspectStack stack) { private long injectToNetwork(IGrid grid, AspectStack stack) {
AEssentiaKey key = AEssentiaKey.of(stack.aspectId()); AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
if (key == null) return 0;
return grid.getStorageService().getInventory().insert( return grid.getStorageService().getInventory().insert(
key, stack.amount(), Actionable.MODULATE, key, stack.amount(), Actionable.MODULATE,
IActionSource.ofMachine(this)); IActionSource.ofMachine(this));
@@ -135,12 +189,23 @@ public class EssentiaImportBusPart extends IOBusPart
public void readFromNBT(CompoundTag extra, HolderLookup.Provider registries) { public void readFromNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.readFromNBT(extra, registries); super.readFromNBT(extra, registries);
this.keyTypeSelection.readFromNBT(extra, registries); this.keyTypeSelection.readFromNBT(extra, registries);
this.buffer = AspectStack.EMPTY;
if (extra.contains(NBT_BUFFER_ASPECT) && extra.getLong(NBT_BUFFER_AMOUNT) > 0) {
ResourceLocation id = ResourceLocation.tryParse(extra.getString(NBT_BUFFER_ASPECT));
if (id != null) {
this.buffer = new AspectStack(id, extra.getLong(NBT_BUFFER_AMOUNT));
}
}
} }
@Override @Override
public void writeToNBT(CompoundTag extra, HolderLookup.Provider registries) { public void writeToNBT(CompoundTag extra, HolderLookup.Provider registries) {
super.writeToNBT(extra, registries); super.writeToNBT(extra, registries);
this.keyTypeSelection.writeToNBT(extra); this.keyTypeSelection.writeToNBT(extra);
if (!this.buffer.isEmpty()) {
extra.putString(NBT_BUFFER_ASPECT, this.buffer.aspectId().toString());
extra.putLong(NBT_BUFFER_AMOUNT, this.buffer.amount());
}
} }
@Override @Override
@@ -156,3 +221,4 @@ public class EssentiaImportBusPart extends IOBusPart
return this.keyTypeSelection; return this.keyTypeSelection;
} }
} }
@@ -1,11 +1,14 @@
package thaumicenergistics.common.parts; package thaumicenergistics.common.parts;
import appeng.api.config.Actionable; import appeng.api.AECapabilities;
import appeng.api.networking.IGrid; import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.security.IActionSource; import appeng.api.networking.security.IActionSource;
import appeng.api.parts.IPartCollisionHelper; import appeng.api.parts.IPartCollisionHelper;
import appeng.api.parts.IPartItem; import appeng.api.parts.IPartItem;
import appeng.api.parts.IPartModel; import appeng.api.parts.IPartModel;
import appeng.api.storage.IStorageMounts;
import appeng.api.storage.IStorageProvider;
import appeng.api.storage.MEStorage;
import appeng.api.util.KeyTypeSelection; import appeng.api.util.KeyTypeSelection;
import appeng.api.util.KeyTypeSelectionHost; import appeng.api.util.KeyTypeSelectionHost;
import appeng.core.settings.TickRates; import appeng.core.settings.TickRates;
@@ -16,22 +19,30 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction; import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntity;
import org.jetbrains.annotations.Nullable;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.AspectStack;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.appeng.AEssentiaKeyType; import thaumicenergistics.common.integration.appeng.AEssentiaKeyType;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.init.ModMenuTypes; import thaumicenergistics.init.ModMenuTypes;
import java.util.Set; import java.util.Set;
/**
* 源质存储总线:把相邻的源质容器(要素罐子 / 奥术蒸馏器 / 荒古炼金炉 / 源质管道)
* 或相邻方块自身暴露的 {@code MEStorage}(例如子网接口)挂载为网络存储,
* 使终端与子网能读写该容器。
* <p>
* 实现方式与 AE2 原版存储总线一致({@link IStorageProvider#mountInventories}),
* 不主动搬运源质。旧实现按输入总线逻辑把容器抽干再插入网络、且忽略插入返回值,
* 导致网络塞不下时源质凭空消失——已移除。
*/
public class EssentiaStorageBusPart extends IOBusPart public class EssentiaStorageBusPart extends IOBusPart
implements KeyTypeSelectionHost { implements IStorageProvider, KeyTypeSelectionHost {
@PartModels @PartModels
public static final ResourceLocation RL_BASE = public static final ResourceLocation RL_BASE =
@@ -51,9 +62,12 @@ public class EssentiaStorageBusPart extends IOBusPart
public static final PartModel MODEL_ON = new PartModel(RL_BASE, RL_ON); public static final PartModel MODEL_ON = new PartModel(RL_BASE, RL_ON);
private static final double IDLE_POWER = 1.0; private static final double IDLE_POWER = 1.0;
private static final int TRANSFER_RATE = 8;
private boolean isWorkingFlag = false; private boolean isWorkingFlag = false;
/** 相邻源质容器的 MEStorage 视图(惰性创建,内部实时解析方块实体)。 */
private EssentiaContainerStorage containerStorage;
private Component storageDescription;
private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection( private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection(
selection -> { }, selection -> { },
keyType -> keyType == AEssentiaKeyType.INSTANCE); keyType -> keyType == AEssentiaKeyType.INSTANCE);
@@ -61,6 +75,8 @@ public class EssentiaStorageBusPart extends IOBusPart
public EssentiaStorageBusPart(IPartItem<?> partItem) { public EssentiaStorageBusPart(IPartItem<?> partItem) {
super(TickRates.StorageBus, Set.of(AEssentiaKeyType.INSTANCE), partItem); super(TickRates.StorageBus, Set.of(AEssentiaKeyType.INSTANCE), partItem);
this.getMainNode().setIdlePowerUsage(IDLE_POWER); this.getMainNode().setIdlePowerUsage(IDLE_POWER);
// 注册为存储提供者,网络挂载时会调用 mountInventories(与 AE2 原版存储总线一致)
this.getMainNode().addService(IStorageProvider.class, this);
} }
@Override @Override
@@ -77,32 +93,81 @@ public class EssentiaStorageBusPart extends IOBusPart
return MODEL_OFF; return MODEL_OFF;
} }
/** 存储总线不做搬运工作:容器由 {@link #mountInventories} 挂载为网络存储。
* 这里只做状态指示(相邻容器有源质 → 亮工作灯)。 */
@Override @Override
protected boolean doBusWork(IGrid grid) { protected boolean doBusWork(appeng.api.networking.IGrid grid) {
this.isWorkingFlag = false; BlockEntity adjacent = getAdjacentBlockEntity();
this.isWorkingFlag = adjacent != null
Direction side = this.getSide(); && thaumicenergistics.common.integration.tc.TCReflection.peekEssentia(adjacent, 1) != null;
if (side == null) return false; return false;
BlockPos adjacentPos = this.getBlockEntity().getBlockPos().relative(side);
Level levelRaw = this.getLevel();
if (!(levelRaw instanceof ServerLevel level)) return false;
if (!level.isLoaded(adjacentPos)) return false;
BlockEntity adjacentBE = level.getBlockEntity(adjacentPos);
if (adjacentBE == null) return false;
AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE);
if (extracted == null || extracted.isEmpty()) return false;
this.insertIntoNetwork(grid, extracted);
this.isWorkingFlag = true;
return true;
} }
private void insertIntoNetwork(IGrid grid, AspectStack stack) { // ===== IStorageProvider:把相邻存储挂载到网络 =====
AEssentiaKey key = AEssentiaKey.of(stack.aspectId());
grid.getStorageService().getInventory().insert( @Override
key, stack.amount(), Actionable.MODULATE, public void mountInventories(IStorageMounts mounts) {
IActionSource.ofMachine(this)); BlockPos adjacentPos = getAdjacentPos();
if (adjacentPos == null) return;
Level level = getLevel();
if (!(level instanceof ServerLevel serverLevel) || !serverLevel.isLoaded(adjacentPos)) return;
// 1) 相邻方块自身暴露 MEStorage(子网接口等)→ 直接挂载,使终端/子网可访问
Direction side = this.getSide();
MEStorage external = serverLevel.getCapability(AECapabilities.ME_STORAGE, adjacentPos,
side != null ? side.getOpposite() : null);
if (external != null) {
mounts.mount(external, 0);
return;
}
// 2) 否则把相邻源质容器(罐子/蒸馏器/荒古炼金炉/管道)包装成 MEStorage 挂载
mounts.mount(getContainerStorage(), 0);
}
private EssentiaContainerStorage getContainerStorage() {
if (containerStorage == null) {
if (storageDescription == null) {
storageDescription = Component.translatable("item.thaumicenergistics.essentia_storage_bus");
}
containerStorage = new EssentiaContainerStorage(this::getAdjacentBlockEntity, storageDescription);
}
return containerStorage;
}
@Nullable
private BlockPos getAdjacentPos() {
Direction side = this.getSide();
if (side == null || this.getBlockEntity() == null) return null;
return this.getBlockEntity().getBlockPos().relative(side);
}
/** 实时解析相邻方块实体(容器可能被替换/移除,挂载视图每次调用重新读取)。 */
@Nullable
private BlockEntity getAdjacentBlockEntity() {
BlockPos pos = getAdjacentPos();
Level level = getLevel();
if (pos == null || level == null || !level.isLoaded(pos)) return null;
return level.getBlockEntity(pos);
}
@Override
public void addToWorld() {
super.addToWorld();
IStorageProvider.requestUpdate(this.getMainNode());
}
@Override
public void onNeighborChanged(net.minecraft.world.level.BlockGetter level, BlockPos pos, BlockPos neighbor) {
super.onNeighborChanged(level, pos, neighbor);
// 相邻方块变化 → 重新挂载(子网接口 ↔ 源质容器的切换)
IStorageProvider.requestUpdate(this.getMainNode());
}
@Override
protected void onMainNodeStateChanged(IGridNodeListener.State state) {
super.onMainNodeStateChanged(state);
IStorageProvider.requestUpdate(this.getMainNode());
} }
@Override @Override
@@ -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
@@ -0,0 +1,199 @@
package thaumicenergistics.common.tiles;
import net.minecraft.core.BlockPos;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IAspectContainer;
import thaumcraft.common.blockentities.InfusionMatrixBlockEntity;
import thaumcraft.common.blockentities.PedestalBlockEntity;
import thaumicenergistics.common.tiles.TileInfusionMonitor.RiskData;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 注魔风险计算:扫描附近的注魔矩阵,按 5 档风险显示。
* <p>
* 判定依据(与 0.2.2.95 {@code InfusionMatrixBlockEntity} 源码一致):
* <ul>
* <li>单次异常判定概率 = instability / 500(每 10 tick 一次)。</li>
* <li>instability = 配方基础不稳定 + 祭坛对称度(上限 25)。</li>
* <li>源质不足会持续推高不稳定 → 源质缺口越大档位越高。</li>
* </ul>
*/
public final class InfusionRiskCalculator {
/** 扫描祭坛的半径。 */
private static final int MATRIX_SEARCH_RADIUS = 16;
/** 源质扫描半径(对应 EssentiaHandler.drainEssentia 的 range=12)。 */
private static final int ESSENTIA_SCAN_RADIUS = 12;
private InfusionRiskCalculator() {}
/** 附近注魔矩阵信息。 */
public record InfusionMatrixInfo(BlockPos matrixPos, InfusionMatrixBlockEntity matrix) {}
/** 扫描指定位置附近的注魔矩阵(取最近的)。 */
public static InfusionMatrixInfo scanNearbyMatrix(Level level, BlockPos center) {
InfusionMatrixBlockEntity nearest = null;
int bestDist = Integer.MAX_VALUE;
for (int dx = -MATRIX_SEARCH_RADIUS; dx <= MATRIX_SEARCH_RADIUS; dx++) {
for (int dz = -MATRIX_SEARCH_RADIUS; dz <= MATRIX_SEARCH_RADIUS; dz++) {
// 注魔矩阵在 y 方向位置固定(中央柱在下方 2 格),扫描 3 层高度
for (int dy = -4; dy <= 4; dy++) {
BlockPos pos = center.offset(dx, dy, dz);
if (level.getBlockEntity(pos) instanceof InfusionMatrixBlockEntity matrix) {
int dist = dx * dx + dz * dz + dy * dy;
if (dist < bestDist) {
bestDist = dist;
nearest = matrix;
}
}
}
}
}
return nearest != null ? new InfusionMatrixInfo(nearest.getBlockPos(), nearest) : null;
}
/**
* 计算风险数据。若祭坛正在注魔,用实时 instability;否则估算:
* 复制 matches() 匹配当前祭坛布局的配方(忽略研究门禁),得出配方基础不稳定 + 祭坛对称度。
*/
public static RiskData computeRisk(Level level, InfusionMatrixInfo info) {
InfusionMatrixBlockEntity matrix = info.matrix();
boolean crafting = matrix.crafting();
int symmetry = matrix.symmetry();
int recipeInstability;
int instability;
if (crafting) {
// 注魔中:instability() 实时(可能被源质不足推高)
instability = Math.min(25, matrix.instability());
recipeInstability = Math.max(0, instability - symmetry);
} else {
// 未开始:估算配方基础不稳定
recipeInstability = estimateRecipeInstability(level, info);
instability = Math.min(25, recipeInstability + symmetry);
}
// 源质缺口:剩余所需 vs 周围可抽
String shortages = "";
int totalNeeded = 0;
AspectList remaining = matrix.remainingEssentia();
if (remaining != null && remaining.size() > 0) {
Map<Aspect, Long> available = scanAvailableEssentia(level, info.matrixPos());
StringBuilder sb = new StringBuilder();
for (Aspect aspect : remaining.aspects()) {
int need = remaining.amount(aspect);
if (need <= 0) continue;
totalNeeded += need;
long have = available.getOrDefault(aspect, 0L);
if (have < need) {
if (!sb.isEmpty()) sb.append(",");
sb.append(aspect.getTag()).append(" 缺 ").append(need - have);
}
}
shortages = sb.toString();
}
int tier = tierFor(instability, shortages, totalNeeded);
return new RiskData(true, crafting, tier, instability, symmetry, recipeInstability,
tierText(tier), shortages, totalNeeded);
}
/** 5 档判定:不稳定度 + 源质缺口强制升级。 */
private static int tierFor(int instability, String shortages, int totalNeeded) {
int tier;
if (instability <= 1) tier = 1;
else if (instability <= 3) tier = 2;
else if (instability <= 6) tier = 3;
else if (instability <= 12) tier = 4;
else tier = 5;
// 源质缺口强制升级
if (!shortages.isEmpty()) {
// 缺口过半(总需 > 0 且缺口文字非空)→ 至少 4 档;严重 → 5 档
if (totalNeeded > 0 && shortages.length() > 20) {
tier = Math.max(tier, 5);
} else {
tier = Math.max(tier, 4);
}
}
return tier;
}
/** 档位对应文字(玩家视角后果)。 */
public static String tierText(int tier) {
return switch (tier) {
case 1 -> "基本安静,偶发弹飞 1 个物品";
case 2 -> "弹飞常见;闪电/咒波几乎不出现";
case 3 -> "弹飞+可能电击;咒波罕见";
case 4 -> "电击/爆炸/咒波都可能;可能销毁物品";
case 5 -> "频繁销毁/爆炸/咒波/精神污染——不要开始";
default -> "";
};
}
/** 未注魔时估算匹配配方的配方基础不稳定(复制 matches 但忽略研究门禁)。 */
private static int estimateRecipeInstability(Level level, InfusionMatrixInfo info) {
InfusionMatrixBlockEntity matrix = info.matrix();
try {
// 取中央柱物品(注魔矩阵正下方 2 格)
PedestalBlockEntity center = level.getBlockEntity(info.matrixPos().below(2)) instanceof PedestalBlockEntity ped
? ped : null;
if (center == null || center.heldStack().isEmpty()) {
return 0;
}
ItemStack central = center.heldStack();
// 收集周围柱物品(与 scanSurroundings 一致的 ±8 范围简化版)
List<ItemStack> components = new ArrayList<>();
for (int dx = -8; dx <= 8; dx++) {
for (int dz = -8; dz <= 8; dz++) {
if (dx == 0 && dz == 0) continue;
for (int dy = 1; dy <= 10; dy++) {
BlockEntity be = level.getBlockEntity(info.matrixPos().offset(dx, -dy, dz));
if (be instanceof PedestalBlockEntity ped && !ped.heldStack().isEmpty()) {
components.add(ped.heldStack());
break;
}
}
}
}
// 找匹配配方(忽略研究:直接比对中央 + 组件,不调 matches 以免研究门禁)
for (Object o : thaumcraft.api.ThaumcraftApi.getCraftingRecipes()) {
if (!(o instanceof thaumcraft.api.crafting.InfusionRecipe recipe)) continue;
if (!recipe.getRecipeInput().is(central.getItem())) continue;
// 简化:中央物品匹配即采用其基础不稳定(组件精确匹配留给 craftingStart 处理)
return recipe.instability(central);
}
} catch (Exception ignored) {
}
return 0;
}
/** 扫描周围 12 格内源质容器,汇总每种源质可抽量。 */
private static Map<Aspect, Long> scanAvailableEssentia(Level level, BlockPos matrixPos) {
Map<Aspect, Long> totals = new ConcurrentHashMap<>();
for (int dx = -ESSENTIA_SCAN_RADIUS; dx <= ESSENTIA_SCAN_RADIUS; dx++) {
for (int dy = -6; dy <= 6; dy++) {
for (int dz = -ESSENTIA_SCAN_RADIUS; dz <= ESSENTIA_SCAN_RADIUS; dz++) {
BlockEntity be = level.getBlockEntity(matrixPos.offset(dx, dy, dz));
if (be == null || be == level.getBlockEntity(matrixPos)) continue;
if (be instanceof IAspectContainer container) {
AspectList aspects = container.getAspects();
if (aspects == null) continue;
for (Aspect aspect : aspects.aspects()) {
int amt = aspects.getAmount(aspect);
if (amt > 0) totals.merge(aspect, (long) amt, Long::sum);
}
}
}
}
}
return totals;
}
}
@@ -16,7 +16,6 @@ import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup; import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.Container;
import net.minecraft.world.SimpleContainer; import net.minecraft.world.SimpleContainer;
import appeng.api.config.Actionable; import appeng.api.config.Actionable;
@@ -47,13 +46,11 @@ import appeng.api.util.AECableType;
import thaumcraft.api.IVisDiscountGear; import thaumcraft.api.IVisDiscountGear;
import thaumcraft.api.IWarpingGear; import thaumcraft.api.IWarpingGear;
import thaumcraft.api.ThaumcraftApi;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList; 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;
@@ -68,6 +65,8 @@ public class TileArcaneAssembler extends ThETileBase
Aspect.EARTH, Aspect.ORDER, Aspect.ENTROPY Aspect.EARTH, Aspect.ORDER, Aspect.ENTROPY
}; };
/** 基础 vis 折扣(0.8 = 80% 成本)。calculateVisDiscounts 从该值起再减护甲折扣;无护甲时即此值。 */
private static final float BASE_VIS_DISCOUNT = 0.8F;
private static final int CVIS_MULTIPLIER = 10; private static final int CVIS_MULTIPLIER = 10;
private static final int MAX_STORED_CVIS = 187 * CVIS_MULTIPLIER; private static final int MAX_STORED_CVIS = 187 * CVIS_MULTIPLIER;
private static final int BASE_TICKS_PER_CRAFT = 20; private static final int BASE_TICKS_PER_CRAFT = 20;
@@ -75,17 +74,26 @@ public class TileArcaneAssembler extends ThETileBase
private static final double ACTIVE_POWER = 1.5; private static final double ACTIVE_POWER = 1.5;
private static final double WARP_POWER_PERCENT = 0.15; private static final double WARP_POWER_PERCENT = 0.15;
private static final int MAX_SPEED_UPGRADES = 4; private static final int MAX_SPEED_UPGRADES = 4;
private static final int VIS_REPLENISH_INTERVAL = 20; /** vis 充能间隔(tick):每 5 tick 充一次,等效 20 cvis/tick,渐进但够快,避免合成长期卡"等待"。 */
private static final int VIS_REPLENISH_INTERVAL = 5;
/** 每次充能上限(cvis/次,每 VIS_REPLENISH_INTERVAL tick 一次 ≈ 20 cVis/tick)。 */
private static final int MAX_CVIS_PER_REPLENISH = 100;
/** 每次实际 vis 变化累计达到该值才调用一次 setChanged(约 4×5=20 tick),避免合成期间 chunk 持续处于 unsaved 状态。 */ /** 每次实际 vis 变化累计达到该值才调用一次 setChanged(约 4×5=20 tick),避免合成期间 chunk 持续处于 unsaved 状态。 */
private static final int SET_CHANGED_INTERVAL = 4; private static final int SET_CHANGED_INTERVAL = 4;
/** 元始珍珠:TC 奥术配方中唯一 retainIngredient 原料(ShapedArcaneRecipe.retainIngredient),合成不消耗。 */
private static final ResourceLocation PRIMORDIAL_PEARL_ID =
ResourceLocation.fromNamespaceAndPath("thaumcraft", "primordial_pearl");
public static final int SLOT_COUNT = 27; public static final int SLOT_COUNT = 31;
public static final int KCORE_SLOT = 0; public static final int KCORE_SLOT = 0;
public static final int PATTERN_SLOT_START = 1; public static final int PATTERN_SLOT_START = 1;
public static final int PATTERN_SLOT_END = 21; public static final int PATTERN_SLOT_END = 21;
public static final int TARGET_SLOT = 22; public static final int TARGET_SLOT = 22;
public static final int ARMOR_SLOT_START = 23; public static final int ARMOR_SLOT_START = 23;
public static final int ARMOR_SLOT_END = 26; public static final int ARMOR_SLOT_END = 26;
/** 加速卡槽:持久化在 Tile 内(此前放容器局部 handler,关 GUI 即丢失→吞卡)。 */
public static final int UPGRADE_SLOT_START = 27;
public static final int UPGRADE_SLOT_END = 30;
protected final IManagedGridNode mainNode; protected final IManagedGridNode mainNode;
private final IActionSource actionSource; private final IActionSource actionSource;
@@ -108,6 +116,15 @@ public class TileArcaneAssembler extends ThETileBase
private boolean isCrafting = false; private boolean isCrafting = false;
private int craftTickCounter = 0; private int craftTickCounter = 0;
private int speedUpgrades = 0; private int speedUpgrades = 0;
/** 当前合成配方的 3×3 材料网格(GUI 右下配方预览用,随合成开始填充、结束清空)。 */
private ItemStack[] previewGrid = emptyPreviewGrid();
/** 全 EMPTY 的 9 格预览数组(避免 null 元素导致网络包编码崩溃)。 */
private static ItemStack[] emptyPreviewGrid() {
ItemStack[] grid = new ItemStack[9];
java.util.Arrays.fill(grid, ItemStack.EMPTY);
return grid;
}
/** /**
* 从内存卡粘贴的 vis_interface P2P 频率;装配器优先从该频率的 vis_interface P2P 端抽 vis。 * 从内存卡粘贴的 vis_interface P2P 频率;装配器优先从该频率的 vis_interface P2P 端抽 vis。
* 0 = 未设置,走 essentia 网络。对应 1.7.10 的 visSourceInfo,但用 P2P 频率替代位置(适配 neo P2P 设计)。 * 0 = 未设置,走 essentia 网络。对应 1.7.10 的 visSourceInfo,但用 P2P 频率替代位置(适配 neo P2P 设计)。
@@ -152,7 +169,7 @@ public class TileArcaneAssembler extends ThETileBase
actionSource = IActionSource.ofMachine(this); actionSource = IActionSource.ofMachine(this);
for (Aspect primal : PRIMALS) { for (Aspect primal : PRIMALS) {
visDiscount.put(primal, 0.8F); visDiscount.put(primal, BASE_VIS_DISCOUNT);
} }
} }
@@ -224,13 +241,12 @@ public class TileArcaneAssembler extends ThETileBase
} }
visTickCounter++; visTickCounter++;
if (visTickCounter >= VIS_REPLENISH_INTERVAL) { // 配置了 vis 源(内存卡设了 VisInterface 频率)就持续充能:放置时要素条也显示充能过程,
// 合成时直接用缓存,不会卡"等待"。未配置源不充能(要素条空)。
if ((isCrafting || visSourceFreq != 0) && visTickCounter >= VIS_REPLENISH_INTERVAL) {
visTickCounter = 0; visTickCounter = 0;
// 仅合成中或 vis 未满时才抽(满时每 5 tick 空转会白跑 P2P/网格查询)
if (isCrafting || !storedVisFull()) {
replenishVis(grid); replenishVis(grid);
} }
}
if (isCrafting) { if (isCrafting) {
return craftingTick(grid, ticksSinceLast); return craftingTick(grid, ticksSinceLast);
@@ -250,46 +266,34 @@ public class TileArcaneAssembler extends ThETileBase
} }
private void replenishVis(IGrid grid) { private void replenishVis(IGrid grid) {
// 优先从内存卡配置的 vis_interface P2P 源抽 vis(1.7.10 vis 源机制,适配 neo P2P) // 充能前提:必须先用内存卡配置 vis 源(VisInterface P2P 频率)。
if (visSourceFreq != 0) { if (visSourceFreq == 0) {
return;
}
appeng.me.service.P2PService p2p = appeng.me.service.P2PService.get(grid); appeng.me.service.P2PService p2p = appeng.me.service.P2PService.get(grid);
if (p2p != null) { if (p2p == null) {
return;
}
appeng.parts.p2p.P2PTunnelPart<?> input = p2p.getInput(visSourceFreq); appeng.parts.p2p.P2PTunnelPart<?> input = p2p.getInput(visSourceFreq);
if (input instanceof thaumicenergistics.common.parts.VisInterfacePart visSource) { if (!(input instanceof thaumicenergistics.common.parts.VisInterfacePart visSource)) {
for (Aspect primal : PRIMALS) {
int needed = MAX_STORED_CVIS - storedVis.getAmount(primal);
if (needed <= 0) continue;
ResourceLocation id = TCReflection.getAspectId(primal);
if (id == null) continue;
int got = visSource.consumeVis(id, needed);
if (got > 0) {
storedVis.add(primal, got);
maybeMarkVisDirty();
}
}
return; // 有 vis 源时只用它(1.7.10 行为:源不可用则不抽)
}
}
return; // 频率设了但找不到 vis_interface P2P 输入 → 不抽 return; // 频率设了但找不到 vis_interface P2P 输入 → 不抽
} }
// 无 vis 源:从 essentia 网络抽(neo 默认行为) // 限速充能。
IEssentiaGrid essentiaGrid = grid.getService(IEssentiaGrid.class); // 关键单位换算:TC 的 consumeVis/consumeEnergizedVis 返回的是 vis(玩家看到的"X 点要素"),
if (essentiaGrid == null) return; // 而装配器 storedVis 用 cvis(×CVIS_MULTIPLIER)。返回的 vis 必须 ×CVIS_MULTIPLIER 才等于 cvis,
// 否则合成按 cvis 判定永远不够(之前少了 10 倍导致卡"等待")。
for (Aspect primal : PRIMALS) { for (Aspect primal : PRIMALS) {
int currentCvis = storedVis.getAmount(primal); int needed = MAX_STORED_CVIS - storedVis.getAmount(primal);
int needed = MAX_STORED_CVIS - currentCvis;
if (needed <= 0) continue; if (needed <= 0) continue;
// 请求的 vis 数 = 需要补齐的 cvis 换算成 vis,且受每次上限约束
int wantVis = Math.max(1, Math.min(needed / CVIS_MULTIPLIER, MAX_CVIS_PER_REPLENISH / CVIS_MULTIPLIER));
ResourceLocation id = TCReflection.getAspectId(primal); ResourceLocation id = TCReflection.getAspectId(primal);
if (id == null) continue; if (id == null) continue;
int gotVis = visSource.consumeVis(id, wantVis);
int neededEssentia = (needed + CVIS_MULTIPLIER - 1) / CVIS_MULTIPLIER; if (gotVis > 0) {
long extracted = essentiaGrid.extractEssentia(id, neededEssentia, false); storedVis.add(primal, gotVis * CVIS_MULTIPLIER);
if (extracted > 0) {
int cvisGained = (int) (extracted * CVIS_MULTIPLIER);
storedVis.add(primal, cvisGained);
maybeMarkVisDirty(); maybeMarkVisDirty();
} }
} }
@@ -309,12 +313,13 @@ public class TileArcaneAssembler extends ThETileBase
private TickRateModulation craftingTick(IGrid grid, int ticksSinceLast) { private TickRateModulation craftingTick(IGrid grid, int ticksSinceLast) {
if (currentResult.isEmpty()) { if (currentResult.isEmpty()) {
isCrafting = false; isCrafting = false;
previewGrid = emptyPreviewGrid();
thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, false);
return TickRateModulation.IDLE; return TickRateModulation.IDLE;
} }
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;
} }
@@ -328,26 +333,46 @@ public class TileArcaneAssembler extends ThETileBase
AEItemKey outputKey = AEItemKey.of(output); AEItemKey outputKey = AEItemKey.of(output);
if (outputKey == null) return TickRateModulation.IDLE; if (outputKey == null) return TickRateModulation.IDLE;
// 元始珍珠保留:TC 唯一 retainIngredient 原料,合成后珍珠退回网络(与奥术合成终端一致)。
// CPU 已按配方从网络扣除珍珠,这里补回 → 净零不消耗。
var retainedPearls = retainedPearlStacks();
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;
} }
// 预检:珍珠类型塞满放不回网络则等待,避免合成完成却丢珍珠
for (ItemStack pearl : retainedPearls) {
AEItemKey pearlKey = AEItemKey.of(pearl);
if (pearlKey == null) continue;
if (storage.getInventory().insert(
pearlKey, pearl.getCount(), Actionable.SIMULATE, actionSource) < pearl.getCount()) {
return TickRateModulation.SAME;
}
}
consumeVisForCraft(); consumeVisForCraft();
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()); // 退回珍珠(数量 = 3×3 配方网格中珍珠总数,通常 1 颗)
for (ItemStack pearl : retainedPearls) {
AEItemKey pearlKey = AEItemKey.of(pearl);
if (pearlKey != null) {
storage.getInventory().insert(pearlKey, pearl.getCount(), Actionable.MODULATE, actionSource);
}
}
isCrafting = false; isCrafting = false;
currentRecipe = null; currentRecipe = null;
currentResult = ItemStack.EMPTY; currentResult = ItemStack.EMPTY;
currentRecipeAspects = new AspectList(); currentRecipeAspects = new AspectList();
previewGrid = emptyPreviewGrid();
internalInventory.setItem(TARGET_SLOT, ItemStack.EMPTY); internalInventory.setItem(TARGET_SLOT, ItemStack.EMPTY);
setChanged(); setChanged();
markForUpdate(); markForUpdate();
thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, false);
return TickRateModulation.URGENT; return TickRateModulation.URGENT;
} }
@@ -359,11 +384,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);
} }
} }
@@ -392,17 +414,32 @@ public class TileArcaneAssembler extends ThETileBase
private int getRequiredCvisForAspect(Aspect aspect) { private int getRequiredCvisForAspect(Aspect aspect) {
if (currentRecipeAspects == null) return 0; if (currentRecipeAspects == null) return 0;
int baseCost = currentRecipeAspects.getAmount(aspect) * CVIS_MULTIPLIER; int baseCost = currentRecipeAspects.getAmount(aspect) * CVIS_MULTIPLIER;
float discount = visDiscount.getOrDefault(aspect, 1.0F); float discount = visDiscount.getOrDefault(aspect, BASE_VIS_DISCOUNT);
return Math.max(0, Math.round(baseCost * discount)); return Math.max(0, Math.round(baseCost * discount));
} }
/**
* 配方 3×3 网格中需保留的元始珍珠栈。TC 奥术配方中珍珠是唯一 retainIngredient 原料:
* 在奥术工作台/奥术合成终端中作为"催化"留在合成格不消耗,装配器合成完成后也要退回网络。
*/
private List<ItemStack> retainedPearlStacks() {
List<ItemStack> pearls = new ArrayList<>();
if (previewGrid == null) return pearls;
for (ItemStack stack : previewGrid) {
if (!stack.isEmpty() && stack.getItem() == net.minecraft.core.registries.BuiltInRegistries.ITEM.get(PRIMORDIAL_PEARL_ID)) {
pearls.add(stack.copy());
}
}
return pearls;
}
private int ticksPerCraft() { private int ticksPerCraft() {
return Math.max(4, BASE_TICKS_PER_CRAFT - (4 * speedUpgrades)); return Math.max(4, BASE_TICKS_PER_CRAFT - (4 * speedUpgrades));
} }
private void calculateVisDiscounts() { private void calculateVisDiscounts() {
for (Aspect primal : PRIMALS) { for (Aspect primal : PRIMALS) {
float discount = 0.8F; float discount = BASE_VIS_DISCOUNT;
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
ItemStack armor = internalInventory.getItem(ARMOR_SLOT_START + i); ItemStack armor = internalInventory.getItem(ARMOR_SLOT_START + i);
@@ -446,19 +483,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);
} }
@@ -484,15 +515,12 @@ public class TileArcaneAssembler extends ThETileBase
} }
return new ArcaneAssemblerPattern(AEItemKey.of(result), null, aspects, result, inputs, return new ArcaneAssemblerPattern(AEItemKey.of(result), null, aspects, result, inputs,
acp.getResearch()); acp.getResearch(), acp.getIngredients());
} }
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;
@@ -501,10 +529,18 @@ public class TileArcaneAssembler extends ThETileBase
this.currentRecipe = arcanePattern.getRecipe(); this.currentRecipe = arcanePattern.getRecipe();
this.currentRecipeAspects = aspects.copy(); this.currentRecipeAspects = aspects.copy();
this.currentResult = result.copy(); this.currentResult = result.copy();
// 配方预览:复制 3×3 材料网格(不足 9 格补空)
this.previewGrid = emptyPreviewGrid();
ItemStack[] grid = arcanePattern.getGrid();
if (grid != null) {
for (int i = 0; i < 9 && i < grid.length; i++) {
this.previewGrid[i] = grid[i] != null ? grid[i].copy() : ItemStack.EMPTY;
}
}
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()); thaumicenergistics.common.network.AssemblerPreviewSyncPacket.send(this, true);
return true; return true;
} }
@@ -536,32 +572,21 @@ public class TileArcaneAssembler extends ThETileBase
return !isCrafting; return !isCrafting;
} }
@Nullable
public IArcaneRecipe findMatchingArcaneRecipe(Container craftingGrid) {
if (level == null) return null;
for (Object recipeObj : ThaumcraftApi.getCraftingRecipes()) {
if (recipeObj instanceof IArcaneRecipe recipe) {
try {
if (recipe.matches(craftingGrid, level, null)) {
return recipe;
}
} catch (Exception ignored) {}
}
}
return null;
}
public boolean isCrafting() { return isCrafting; } public boolean isCrafting() { return isCrafting; }
public int getSpeedUpgrades() { return speedUpgrades; } public int getSpeedUpgrades() { return speedUpgrades; }
public int getCraftTickCounter() { return craftTickCounter; } public int getCraftTickCounter() { return craftTickCounter; }
public int getTicksPerCraft() { return ticksPerCraft(); } public int getTicksPerCraft() { return ticksPerCraft(); }
/** 当前合成配方的 3×3 材料网格(GUI 右下配方预览;未合成时为空数组)。 */
public ItemStack[] getPreviewGrid() { return previewGrid; }
/** 当前合成产物(GUI 预产物显示;未合成为空)。 */
public ItemStack getCurrentResult() { return currentResult; }
public float getCraftProgress() { public float getCraftProgress() {
if (!isCrafting || ticksPerCraft() <= 0) return 0f; if (!isCrafting || ticksPerCraft() <= 0) return 0f;
return Math.min(1f, (float) craftTickCounter / ticksPerCraft()); return Math.min(1f, (float) craftTickCounter / ticksPerCraft());
} }
public AspectList getStoredVis() { return storedVis; } public AspectList getStoredVis() { return storedVis; }
public SimpleContainer getInternalInventory() { return internalInventory; } public SimpleContainer getInternalInventory() { return internalInventory; }
public float getVisDiscountForAspect(Aspect aspect) { return visDiscount.getOrDefault(aspect, 1.0F); } public float getVisDiscountForAspect(Aspect aspect) { return visDiscount.getOrDefault(aspect, BASE_VIS_DISCOUNT); }
public float getWarpPowerMultiplier() { return warpPowerMultiplier; } public float getWarpPowerMultiplier() { return warpPowerMultiplier; }
public void setSpeedUpgrades(int count) { public void setSpeedUpgrades(int count) {
@@ -570,6 +595,8 @@ public class TileArcaneAssembler extends ThETileBase
} }
public short getVisSourceFreq() { return visSourceFreq; } public short getVisSourceFreq() { return visSourceFreq; }
/** ME 节点是否有能量(Jade 网络状态细分用)。 */
public boolean isNodePowered() { return mainNode != null && mainNode.isPowered(); }
/** /**
* 内存卡右键装配器:读 P2P 频率,设为 vis 源。 * 内存卡右键装配器:读 P2P 频率,设为 vis 源。
@@ -608,19 +635,30 @@ 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);
} }
} }
if (slot >= ARMOR_SLOT_START && slot <= ARMOR_SLOT_END) { if (slot >= ARMOR_SLOT_START && slot <= ARMOR_SLOT_END) {
calculateVisDiscounts(); calculateVisDiscounts();
} }
if (slot >= UPGRADE_SLOT_START && slot <= UPGRADE_SLOT_END) {
recountSpeedUpgrades();
}
if (slot >= PATTERN_SLOT_START && slot <= PATTERN_SLOT_END) { if (slot >= PATTERN_SLOT_START && slot <= PATTERN_SLOT_END) {
stalePatterns = true; stalePatterns = true;
} }
setChanged(); setChanged();
} }
/** 按 Tile 内加速卡槽的实际卡片数量刷新 speedUpgrades(持久化计数,不再依赖菜单存活)。 */
private void recountSpeedUpgrades() {
int count = 0;
for (int i = UPGRADE_SLOT_START; i <= UPGRADE_SLOT_END; i++) {
if (!internalInventory.getItem(i).isEmpty()) count++;
}
setSpeedUpgrades(count);
}
private void updatePatternSlots() { private void updatePatternSlots() {
ItemStack kCore = internalInventory.getItem(KCORE_SLOT); ItemStack kCore = internalInventory.getItem(KCORE_SLOT);
if (kCore.isEmpty() || !kCore.is(ModItems.KNOWLEDGE_CORE.get())) { if (kCore.isEmpty() || !kCore.is(ModItems.KNOWLEDGE_CORE.get())) {
@@ -661,6 +699,15 @@ public class TileArcaneAssembler extends ThETileBase
tag.put("Slot" + i, stack.save(reg)); tag.put("Slot" + i, stack.save(reg));
} }
} }
// 配方预览 3×3 材料(GUI 右下角显示)
CompoundTag gridTag = new CompoundTag();
for (int i = 0; i < 9; i++) {
ItemStack stack = previewGrid[i];
if (stack != null && !stack.isEmpty()) {
gridTag.put("g" + i, stack.save(reg));
}
}
tag.put("PreviewGrid", gridTag);
return tag; return tag;
} }
@@ -677,6 +724,15 @@ public class TileArcaneAssembler extends ThETileBase
internalInventory.setItem(i, ItemStack.EMPTY); internalInventory.setItem(i, ItemStack.EMPTY);
} }
} }
previewGrid = emptyPreviewGrid();
if (tag.contains("PreviewGrid")) {
CompoundTag gridTag = tag.getCompound("PreviewGrid");
for (int i = 0; i < 9; i++) {
if (gridTag.contains("g" + i)) {
previewGrid[i] = ItemStack.parseOptional(reg, gridTag.getCompound("g" + i));
}
}
}
} finally { } finally {
loadingFromClientTag = false; loadingFromClientTag = false;
} }
@@ -717,6 +773,7 @@ public class TileArcaneAssembler extends ThETileBase
updatePatternSlots(); updatePatternSlots();
stalePatterns = true; stalePatterns = true;
calculateVisDiscounts(); calculateVisDiscounts();
recountSpeedUpgrades();
} }
@Override @Override
@@ -753,21 +810,30 @@ public class TileArcaneAssembler extends ThETileBase
private final ItemStack resultStack; private final ItemStack resultStack;
private final IInput[] inputs; private final IInput[] inputs;
private final String research; private final String research;
/** 3×3 配方网格(知识核心存储的 9 格材料,GUI 配方预览用)。 */
private final ItemStack[] grid;
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe, public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack, AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks) { List<GenericStack> inputStacks) {
this(definition, recipe, aspects, resultStack, inputStacks, null); this(definition, recipe, aspects, resultStack, inputStacks, null, null);
} }
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe, public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack, AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks, String research) { List<GenericStack> inputStacks, String research) {
this(definition, recipe, aspects, resultStack, inputStacks, research, null);
}
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
AspectList aspects, ItemStack resultStack,
List<GenericStack> inputStacks, String research, ItemStack[] grid) {
this.definition = definition; this.definition = definition;
this.recipe = recipe; this.recipe = recipe;
this.aspects = aspects; this.aspects = aspects;
this.resultStack = resultStack; this.resultStack = resultStack;
this.research = research; this.research = research;
this.grid = grid;
this.inputs = new IInput[inputStacks.size()]; this.inputs = new IInput[inputStacks.size()];
for (int i = 0; i < inputStacks.size(); i++) { for (int i = 0; i < inputStacks.size(); i++) {
GenericStack stack = inputStacks.get(i); GenericStack stack = inputStacks.get(i);
@@ -792,6 +858,13 @@ public class TileArcaneAssembler extends ThETileBase
@Nullable @Nullable
@Override @Override
public AEKey getRemainingKey(AEKey what) { public AEKey getRemainingKey(AEKey what) {
// 元始珍珠作为"催化"原料:声明合成后它会返回网络(AE2 原生容器机制)。
// 规划端据此按"每次 1 单"顺序模拟、执行端把珍珠计入 CPU waitingFor,
// 机器合成完成把珍珠退回网络后 CPU 自动捕获续下一单——1 颗珍珠可连续多单。
if (what instanceof AEItemKey itemKey
&& itemKey.getItem() == net.minecraft.core.registries.BuiltInRegistries.ITEM.get(PRIMORDIAL_PEARL_ID)) {
return what;
}
return null; return null;
} }
}; };
@@ -829,6 +902,9 @@ public class TileArcaneAssembler extends ThETileBase
public IArcaneRecipe getRecipe() { return recipe; } public IArcaneRecipe getRecipe() { return recipe; }
public AspectList getAspects() { return aspects; } public AspectList getAspects() { return aspects; }
public ItemStack getResultStack() { return resultStack; } public ItemStack getResultStack() { return resultStack; }
/** 3×3 配方网格(可能为 null/短数组,GUI 预览用)。 */
@Nullable
public ItemStack[] getGrid() { return grid; }
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */ /** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
@Nullable @Nullable
public String getResearch() { return research; } public String getResearch() { return research; }
@@ -8,7 +8,6 @@ import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.IEssentiaTransport; import thaumcraft.api.aspects.IEssentiaTransport;
import thaumcraft.common.essentia.EssentiaTransport; import thaumcraft.common.essentia.EssentiaTransport;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.api.storage.IAspectStack; import thaumicenergistics.api.storage.IAspectStack;
import thaumicenergistics.init.ModBlockEntities; import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModItems; import thaumicenergistics.init.ModItems;
@@ -33,7 +32,64 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override @Override
protected double getIdlePowerUsage() { protected double getIdlePowerUsage() {
return 3.0; // 基础 3 + 每连接 1 个接收器 +5
return 3.0 + 5.0 * getLinkedReceiverCount();
}
// ===== 无线接收器连接管理 =====
/** 最多连接的接收器数量。 */
public static final int MAX_LINKED_RECEIVERS = 8;
/** 最大连接距离(格)。 */
public static final int MAX_LINK_DISTANCE = 32;
private List<BlockPos> linkedReceivers = new java.util.ArrayList<>();
public java.util.List<BlockPos> getLinkedReceivers() {
return linkedReceivers;
}
public int getLinkedReceiverCount() {
return linkedReceivers.size();
}
/** 注册一个接收器连接(校验数量与距离,失败返回原因文案;成功返回 null)。 */
public String addLinkedReceiver(BlockPos receiverPos) {
if (linkedReceivers.contains(receiverPos)) return null; // 已连接
if (linkedReceivers.size() >= MAX_LINKED_RECEIVERS) {
return "§c该源质供应器已连接满 " + MAX_LINKED_RECEIVERS + " 个接收器";
}
double dist = Math.sqrt(worldPosition.distSqr(receiverPos));
if (dist > MAX_LINK_DISTANCE) {
return "§c距离过远(" + (int) Math.ceil(dist) + " 格 > " + MAX_LINK_DISTANCE + " 格上限)";
}
linkedReceivers.add(receiverPos);
setChanged();
updatePower();
return null;
}
public void removeLinkedReceiver(BlockPos receiverPos) {
if (linkedReceivers.remove(receiverPos)) {
setChanged();
updatePower();
}
}
/** 周期校验:清除已失效的接收器连接(方块被拆除)。 */
public void validateLinkedReceivers() {
if (level == null || linkedReceivers.isEmpty()) return;
linkedReceivers.removeIf(pos -> {
boolean gone = !(level.getBlockEntity(pos) instanceof TileEssentiaProviderConnection);
if (gone) setChanged();
return gone;
});
updatePower();
}
private void updatePower() {
if (mainNode != null) {
mainNode.setIdlePowerUsage(getIdlePowerUsage());
}
} }
/** getEssentiaList 短时缓存:TC 管道 suction 每 tick 调 getEssentiaType,避免每 tick 全量遍历。 */ /** getEssentiaList 短时缓存:TC 管道 suction 每 tick 调 getEssentiaType,避免每 tick 全量遍历。 */
@@ -55,6 +111,12 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
if (essentiaListCacheTimer > 0) essentiaListCacheTimer--; if (essentiaListCacheTimer > 0) essentiaListCacheTimer--;
// 周期校验无线接收器连接(约每 2 秒)
if (++validateCounter >= 40) {
validateCounter = 0;
validateLinkedReceivers();
}
if (++tickCount >= tickRate) { if (++tickCount >= tickRate) {
tickCount = 0; tickCount = 0;
tickRate = TICK_RATE_IDLE; tickRate = TICK_RATE_IDLE;
@@ -62,6 +124,8 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
} }
} }
private int validateCounter = 0;
// ===== IEssentiaTransport ===== // ===== IEssentiaTransport =====
@Override @Override
@@ -95,11 +159,8 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override @Override
public int takeEssentia(Aspect aspect, int amount, Direction side) { public int takeEssentia(Aspect aspect, int amount, Direction side) {
ThaumicEnergistics.LOG.debug("[MyHano-E] takeEssentia: aspect={}, amount={}, side={}, active={}",
aspect != null ? aspect.getTag() : "null", amount, side, mainNode.isActive());
if (!mainNode.isActive()) return 0; if (!mainNode.isActive()) return 0;
long extracted = extractEssentiaFromNetwork(aspect, amount, false); long extracted = extractEssentiaFromNetwork(aspect, amount, false);
ThaumicEnergistics.LOG.debug("[MyHano-E] takeEssentia result={}", extracted);
if (extracted > 0) { if (extracted > 0) {
tickRate = TICK_RATE_URGENT; tickRate = TICK_RATE_URGENT;
} }
@@ -108,13 +169,10 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
@Override @Override
public int addEssentia(Aspect aspect, int amount, Direction side) { public int addEssentia(Aspect aspect, int amount, Direction side) {
ThaumicEnergistics.LOG.debug("[MyHano-E] addEssentia: aspect={}, amount={}, side={}, active={}",
aspect != null ? aspect.getTag() : "null", amount, side, mainNode.isActive());
if (!mainNode.isActive()) return 0; if (!mainNode.isActive()) return 0;
long rejected = injectEssentiaToNetwork(aspect, amount, false); long rejected = injectEssentiaToNetwork(aspect, amount, false);
long accepted = amount - rejected; long accepted = amount - rejected;
if (accepted > 0) { if (accepted > 0) {
ThaumicEnergistics.LOG.debug("[MyHano-E] addEssentia accepted={}", accepted);
tickRate = TICK_RATE_URGENT; tickRate = TICK_RATE_URGENT;
} }
return (int) accepted; return (int) accepted;
@@ -151,6 +209,36 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
return 1; return 1;
} }
// ===== 供源质无线接收器转发的公开方法 =====
public boolean thE_isActive() {
return mainNode.isActive();
}
public long thE_queryAmount(Aspect aspect) {
return getAspectAmountInNetwork(aspect);
}
public long thE_extract(Aspect aspect, long amount, boolean simulate) {
return extractEssentiaFromNetwork(aspect, amount, simulate);
}
public long thE_inject(Aspect aspect, long amount, boolean simulate) {
return injectEssentiaToNetwork(aspect, amount, simulate);
}
/** 网络中第一种有存量的源质(供接收器无特定需求时兜底)。 */
public Aspect thE_firstAvailable() {
if (essentiaGrid != null) {
for (var stack : getCachedEssentiaList()) {
if (stack.amount() > 0) {
return Aspect.getAspect(stack.aspectId().getPath());
}
}
}
return null;
}
@Override @Override
public boolean renderExtendedTube() { public boolean renderExtendedTube() {
return false; return false;
@@ -199,4 +287,27 @@ public class TileEssentiaProvider extends TileProviderBase implements IEssentiaT
} }
} }
} }
// ===== NBT =====
@Override
protected void saveAdditional(net.minecraft.nbt.CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag();
for (BlockPos pos : linkedReceivers) {
list.add(net.minecraft.nbt.LongTag.valueOf(pos.asLong()));
}
tag.put("LinkedReceivers", list);
}
@Override
protected void loadAdditional(net.minecraft.nbt.CompoundTag tag, net.minecraft.core.HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
linkedReceivers.clear();
if (tag.contains("LinkedReceivers")) {
for (net.minecraft.nbt.Tag t : tag.getList("LinkedReceivers", net.minecraft.nbt.Tag.TAG_LONG)) {
linkedReceivers.add(BlockPos.of(((net.minecraft.nbt.LongTag) t).getAsLong()));
}
}
}
} }
@@ -0,0 +1,326 @@
package thaumicenergistics.common.tiles;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import thaumcraft.api.aspects.Aspect;
import thaumcraft.api.aspects.AspectList;
import thaumcraft.api.aspects.IEssentiaTransport;
import thaumcraft.common.essentia.EssentiaTransport;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModBlocks;
/**
* 源质无线接收器:通过无线连接器与源质供应器建立连接后,
* 作为纯中继(无 AE 节点),把 TC 管道/炼金塔的取源质请求转发到连接的供应器。
*/
public class TileEssentiaProviderConnection extends BlockEntity implements IEssentiaTransport, EssentiaTransport {
/** 连接的源质供应器坐标(null = 未连接)。 */
private BlockPos linkedProviderPos;
/** 接收器每次输入/输出源质上限。 */
public static final int TRANSFER_LIMIT = 16;
/**
* 待注入 AE 网络的源质缓冲。
* <p>
* 网络装不下的源质一律留在这里(随 NBT 持久化),等网络有空间再注入——
* 旧实现直接 {@code provider.thE_inject(...)} 且忽略返回值,
* 而供应器侧一旦把源质写进内存 monitor 就"全部接受",网络无存储时源质凭空消失。
*/
private final AspectList buffer = new AspectList();
private static final String NBT_BUFFER = "ThEBuffer";
private int linkCheckCounter = 0;
private int pullCounter = 0;
private static final int PULL_INTERVAL = 10;
/** 模拟管道的最大扫描距离(格):接收器可隔空拉取该距离内的源质源。 */
private static final int PIPE_SCAN_RANGE = 16;
/** 服务端 tick:周期校验连接 + 主动沿模拟管道拉取源质(蒸馏器/罐/管道不主动推,需接收器拉)。 */
public static void serverTick(net.minecraft.world.level.Level level, BlockPos pos, BlockState state, TileEssentiaProviderConnection be) {
if (level.isClientSide()) return;
if (be.linkedProviderPos != null && ++be.linkCheckCounter >= 40) {
be.linkCheckCounter = 0;
be.getLinkedProvider(); // 无效连接会被清除
}
if (++be.pullCounter >= PULL_INTERVAL) {
be.pullCounter = 0;
be.flushBuffer(); // 先把上次暂存的源质注入网络
be.takeEssentiaFromNeighbors(); // 缓冲清空(网络有余量)时才继续拉取
}
}
/**
* 把缓冲里的源质注入连接的网络。网络满时保留剩余部分,下次再试。
* @return 本次是否有源质被网络接收
*/
private boolean flushBuffer() {
if (buffer.isEmpty()) return false;
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return false;
boolean moved = false;
for (Aspect aspect : buffer.getAspects()) {
if (aspect == null) continue;
int amt = buffer.getAmount(aspect);
if (amt <= 0) continue;
long rejected = provider.thE_inject(aspect, amt, false);
int accepted = amt - (int) rejected;
if (accepted > 0) {
int left = amt - accepted;
if (left > 0) {
buffer.set(aspect, left);
} else {
buffer.aspects.remove(aspect);
}
moved = true;
}
if (rejected > 0) break; // 网络已满:剩余留在缓冲里
}
if (moved) setChanged();
return moved;
}
/** 模拟管道:沿 6 个方向扫描,最近的有效源质源(蒸馏器/罐/管道)隔空拉取,经连接供应器注入 AE 网络。 */
private void takeEssentiaFromNeighbors() {
if (level == null || level.isClientSide()) return;
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return;
// 缓冲还没清空 = 网络装不下,先别再吸(否则"吸走却存不下")
if (!buffer.isEmpty()) return;
for (Direction side : Direction.values()) {
if (!canInputFrom(side)) continue;
// 沿方向行走:非传输方块 = 管道断;空管道 = 继续;有源质 = 拉取
for (int d = 1; d <= PIPE_SCAN_RANGE; d++) {
BlockPos sourcePos = worldPosition.relative(side, d);
if (!level.isLoaded(sourcePos)) break;
BlockEntity sourceTile = level.getBlockEntity(sourcePos);
if (sourceTile == this) break;
if (sourceTile instanceof TileEssentiaProvider) break; // 到供应器本体,防循环
if (!(sourceTile instanceof IEssentiaTransport source)) break; // 非传输方块 = 管道断
Direction sourceSide = side.getOpposite();
if (!source.canOutputTo(sourceSide)) break; // 不能向接收器方向输出 = 断
int srcAmount = source.getEssentiaAmount(sourceSide);
if (srcAmount <= 0) continue; // 空管道/空源 → 继续走
int dSuck = getSuctionAmount(side);
if (dSuck < source.getMinimumSuction() || dSuck < source.getSuctionAmount(sourceSide)) {
break;
}
Aspect sourceAspect = source.getEssentiaType(sourceSide);
if (sourceAspect == null) break;
// 预检:网络能收多少(rejected = 收不下的量);一点都收不下就完全不抽取
int want = Math.min(Math.max(srcAmount, 1), TRANSFER_LIMIT);
long rejected = provider.thE_inject(sourceAspect, want, true);
int room = want - (int) rejected;
if (room <= 0) break;
int taken = source.takeEssentia(sourceAspect, room, sourceSide);
if (taken > 0) {
buffer.add(sourceAspect, taken); // 先进缓冲,注入失败也不丢
flushBuffer();
}
break; // 拉到即停
}
}
}
public TileEssentiaProviderConnection(BlockPos pos, BlockState state) {
super(ModBlockEntities.ESSENTIA_PROVIDER_CONNECTION.get(), pos, state);
}
// ===== 连接管理 =====
public BlockPos getLinkedProviderPos() {
return linkedProviderPos;
}
public boolean isLinked() {
return linkedProviderPos != null && getLinkedProvider() != null;
}
/** 设置连接(无线连接器右键供应器时调用)。返回 null = 成功;否则返回失败原因文案。 */
public String setLinkedProvider(BlockPos providerPos) {
// 在供应器侧注册(校验数量/距离)
if (level != null && !level.isClientSide()) {
if (level.getBlockEntity(providerPos) instanceof TileEssentiaProvider provider) {
String error = provider.addLinkedReceiver(worldPosition);
if (error != null) {
return error; // 超限/超距,不建立连接
}
} else {
return "§c目标不是有效的源质供应器";
}
}
this.linkedProviderPos = providerPos;
setChanged();
updateConnectedState();
return null;
}
public void clearLink() {
// 从供应器侧注销
if (level != null && !level.isClientSide() && linkedProviderPos != null
&& level.getBlockEntity(linkedProviderPos) instanceof TileEssentiaProvider provider) {
provider.removeLinkedReceiver(worldPosition);
}
this.linkedProviderPos = null;
setChanged();
updateConnectedState();
}
private void updateConnectedState() {
if (level == null || level.isClientSide()) return;
boolean connected = linkedProviderPos != null;
BlockState st = getBlockState();
if (st.getValue(ModBlocks.CONNECTED) != connected) {
level.setBlock(worldPosition, st.setValue(ModBlocks.CONNECTED, connected), 3);
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
/** 查找连接的供应器 tile。 */
public TileEssentiaProvider getLinkedProvider() {
if (level == null || level.isClientSide() || linkedProviderPos == null) return null;
if (level.getBlockEntity(linkedProviderPos) instanceof TileEssentiaProvider provider) {
// 自愈:供应器列表缺本接收器时补注册(修复旧存档/列表丢失导致的状态不一致)
if (!provider.getLinkedReceivers().contains(worldPosition)) {
provider.addLinkedReceiver(worldPosition);
}
return provider;
}
// 供应器被移除 → 清除连接
linkedProviderPos = null;
setChanged();
updateConnectedState();
return null;
}
// ===== IEssentiaTransport(转发到连接供应器) =====
@Override
public boolean isConnectable(Direction side) {
return true;
}
@Override
public boolean canInputFrom(Direction side) {
return true; // 支持输入源质到 AE 网络
}
@Override
public boolean canOutputTo(Direction side) {
return true;
}
@Override
public void setSuction(Aspect aspect, int amount) {
}
@Override
public Aspect getSuctionType(Direction side) {
return null;
}
@Override
public int getSuctionAmount(Direction side) {
// 与源质供应器一致:有吸力才能被蒸馏器等设施注入源质(输入功能)
return 8;
}
@Override
public int takeEssentia(Aspect aspect, int amount, Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0;
return (int) provider.thE_extract(aspect, Math.min(amount, TRANSFER_LIMIT), false);
}
@Override
public int addEssentia(Aspect aspect, int amount, Direction side) {
// 无线输入:先问网络能收多少,只接收装得下的部分(其余留在管道/容器里,绝不吞掉)。
// 接收后先进缓冲,注入失败下次重试——保证源质不丢。
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive() || aspect == null || amount <= 0) return 0;
int want = Math.min(amount, TRANSFER_LIMIT);
long rejected = provider.thE_inject(aspect, want, true);
int room = want - (int) rejected;
if (room <= 0) return 0; // 网络无源质存储或已满 → 拒收
buffer.add(aspect, room);
flushBuffer();
setChanged();
return room;
}
@Override
public int getEssentiaAmount(Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return 0;
Aspect wanted = getNeighborWantedAspect(side);
if (wanted != null) {
return (int) Math.min(provider.thE_queryAmount(wanted), Integer.MAX_VALUE);
}
return 0;
}
@Override
public Aspect getEssentiaType(Direction side) {
TileEssentiaProvider provider = getLinkedProvider();
if (provider == null || !provider.thE_isActive()) return null;
Aspect wanted = getNeighborWantedAspect(side);
if (wanted != null && provider.thE_queryAmount(wanted) > 0) {
return wanted;
}
return provider.thE_firstAvailable();
}
@Override
public int getMinimumSuction() {
return 0;
}
@Override
public boolean renderExtendedTube() {
return false;
}
private Aspect getNeighborWantedAspect(Direction side) {
BlockPos neighborPos = worldPosition.relative(side);
if (level.getBlockEntity(neighborPos) instanceof IEssentiaTransport transport) {
return transport.getSuctionType(side.getOpposite());
}
return null;
}
// ===== NBT =====
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.saveAdditional(tag, registries);
if (linkedProviderPos != null) {
tag.putLong("LinkedProvider", linkedProviderPos.asLong());
}
if (!buffer.isEmpty()) {
buffer.writeToNBT(tag, NBT_BUFFER);
}
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
super.loadAdditional(tag, registries);
if (tag.contains("LinkedProvider")) {
linkedProviderPos = BlockPos.of(tag.getLong("LinkedProvider"));
} else {
linkedProviderPos = null;
}
buffer.aspects.clear();
if (tag.contains(NBT_BUFFER)) {
buffer.readFromNBT(tag, NBT_BUFFER);
}
}
}
@@ -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
} }
} }
@@ -0,0 +1,604 @@
package thaumicenergistics.common.tiles;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.Container;
import net.minecraft.world.Containers;
import net.minecraft.world.SimpleContainer;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.Display;
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.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.neoforged.neoforge.common.extensions.IPlayerExtension;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.EnumSet;
import java.util.List;
import appeng.api.networking.GridFlags;
import appeng.api.networking.GridHelper;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.IManagedGridNode;
import appeng.api.networking.IInWorldGridNodeHost;
import appeng.api.util.AECableType;
import net.minecraft.world.item.Items;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.init.ModBlockEntities;
import thaumicenergistics.init.ModBlocks;
/**
* 注魔监视器(灵气观测器)方块实体。
* <ul>
* <li>1 格物品槽:Shift+右键放入魔导手册(book 状态)。</li>
* <li>AE2 网格节点:连接 ME 网络后 network 状态=true。</li>
* <li>每 tick 扫描附近注魔矩阵,缓存风险数据供 Jade 显示(5 档风险/不稳定度/档位文字)。</li>
* </ul>
* 方块状态:
* <ol>
* <li>默认 → off 模型</li>
* <li>放手册 → off + book</li>
* <li>手册 + 连网 → off + book + on</li>
* <li>连网(无手册)→ off + Jade 显示</li>
* </ol>
*/
public class TileInfusionMonitor extends ThETileBase implements IInWorldGridNodeHost {
/** 手册槽位。 */
public static final int BOOK_SLOT = 0;
private static final int SCAN_INTERVAL = 10; // 每 10 tick 验证一次祭坛(缓存命中时仅 1 次查询)
/** 未找到祭坛时的扫描退避间隔(tick):全量扫描约 9800 次 getBlockEntity,附近无祭坛时降低频率控 mspt。 */
private static final int SCAN_MISS_INTERVAL = 200;
/** 风险重算限频(tick):computeRisk 内部也要扫上千次 getBlockEntity(配方估算/源质汇总),控制 mspt。 */
private static final int RISK_INTERVAL = 40;
protected final IManagedGridNode mainNode;
private final SimpleContainer inventory = new SimpleContainer(1) {
@Override
public void setChanged() {
super.setChanged();
TileInfusionMonitor.this.setChanged();
TileInfusionMonitor.this.updateBookState();
}
};
private int scanCounter = 0;
private int riskCounter = 0;
private BlockPos matrixPos = null;
/** 上次扫描到的祭坛注魔中状态(翻转时强制立即重算风险,抓住注魔启动瞬间的高不稳定度)。 */
private boolean cachedCrafting = false;
/** 区块卸载/方块移除中:禁止 setBlock / markForUpdate(卸载时改方块状态会触发区块重载,停服保存死循环)。 */
private boolean unloading = false;
/** 上一次写入气泡的文本(内容未变则跳过反射/NBT 重建,避免每 tick 开销)。 */
private net.minecraft.network.chat.Component lastBubbleText = null;
/** 缓存的注魔风险数据(弹窗显示)。 */
private RiskData cachedRisk = RiskData.NONE;
/** 悬浮气泡实体(text_display,服务端引用)。 */
private net.minecraft.world.entity.Display.TextDisplay bubble;
/** 气泡实体 Tag(用于坐标匹配查找)。 */
private static final String BUBBLE_TAG = "thaumicenergistics:monitor_bubble";
/** 气泡背景:半透明深灰(ARGB,Alpha=176)。 */
private static final int BUBBLE_BG_COLOR = 0xB0333333;
/** 气泡行宽(换行宽度)。中文全角字符约 12px,最长行"不稳定度: 9 (基础 5 + 祭坛 4)"约 240px——
* 必须大于最长行,否则行尾空格被换行逻辑丢弃、背景只包住分段 → 右侧看似截断。 */
private static final int BUBBLE_LINE_WIDTH = 320;
private final IGridNodeListener<TileInfusionMonitor> nodeListener = new IGridNodeListener<>() {
@Override
public void onSaveChanges(TileInfusionMonitor owner, IGridNode node) {
owner.setChanged();
}
@Override
public void onStateChanged(TileInfusionMonitor owner, IGridNode node, State state) {
if (state == State.POWER) {
owner.updateNetworkState();
}
owner.markForUpdate();
}
};
public TileInfusionMonitor(BlockPos pos, BlockState state) {
super(ModBlockEntities.INFUSION_MONITOR.get(), pos, state);
this.mainNode = GridHelper.createManagedNode(this, nodeListener)
.setVisualRepresentation(thaumicenergistics.init.ModItems.INFUSION_MONITOR_ITEM.get())
.setInWorldNode(true)
.setTagName("proxy")
.setFlags(GridFlags.REQUIRE_CHANNEL)
.setExposedOnSides(EnumSet.allOf(Direction.class));
this.mainNode.setIdlePowerUsage(40.0);
}
@Override
public void onLoad() {
super.onLoad();
unloading = false;
if (level != null && !level.isClientSide()) {
mainNode.create(level, getBlockPos());
}
updateBookState();
updateNetworkState();
}
@Override
public void setRemoved() {
unloading = true; // 先置标志再销毁网格节点:节点销毁回调会触发 updateNetworkState
super.setRemoved();
if (level != null && !level.isClientSide()) {
discardBubble();
}
if (mainNode != null) {
mainNode.destroy();
}
}
@Override
public void onChunkUnloaded() {
unloading = true; // 同上:卸载期间禁止改方块状态,否则 chunk 被重新标记脏 → 保存循环
super.onChunkUnloaded();
if (mainNode != null) {
mainNode.destroy();
}
}
@Override
public void onBreakBlock() {
super.onBreakBlock();
if (level != null && !level.isClientSide) {
discardBubble();
Containers.dropContents(level, worldPosition, inventory);
}
}
// ===== tick / 扫描 =====
public static void tick(Level level, BlockPos pos, BlockState state, TileInfusionMonitor be) {
if (level.isClientSide()) return;
// 无祭坛缓存时全量扫描退避到 SCAN_MISS_INTERVAL(否则每 10 tick 扫 9800 次 getBlockEntity,
// 附近没建祭坛时也持续 ~70us/t)
int interval = be.matrixPos == null ? SCAN_MISS_INTERVAL : SCAN_INTERVAL;
if (++be.scanCounter >= interval) {
be.scanCounter = 0;
be.scanMatrix();
}
be.updateNetworkState();
be.updateBubble();
}
/** 扫描附近注魔矩阵并缓存风险数据。
* 快速路径:已缓存矩阵仍在 → 只做 1 次 getBlockEntity 验证;
* 全量扫描(33×9×33≈9800 次 getBlockEntity)仅缓存失效时执行,是 mspt 大户。
* 风险重算限频(配方估算/源质汇总还要上千次查询),但注魔开始/结束瞬间(crafting 翻转)强制立即重算。 */
private void scanMatrix() {
BlockPos found = matrixPos;
boolean craftingChanged = false;
// 快速路径:缓存矩阵仍在 → 跳过全量扫描
if (found != null) {
if (level.getBlockEntity(found) instanceof thaumcraft.common.blockentities.InfusionMatrixBlockEntity matrix) {
boolean crafting = matrix.crafting();
craftingChanged = crafting != cachedCrafting;
cachedCrafting = crafting;
} else {
found = null; // 缓存失效 → 走全量扫描
}
}
if (found == null) {
InfusionRiskCalculator.InfusionMatrixInfo info = InfusionRiskCalculator.scanNearbyMatrix(level, worldPosition);
found = info != null ? info.matrixPos() : null;
}
RiskData newRisk = cachedRisk;
if (found != null) {
// 按 tick 累计(每轮扫描间隔 SCAN_INTERVAL);首次发现即时重算(覆盖层/气泡 0.5 秒内出现),
// 之后每 RISK_INTERVAL tick 重算一次(按"扫描轮数"计数会变成 40 轮×10 tick=20 秒才出数据);
// 注魔开始/结束瞬间(crafting 翻转)也立即重算,抓住启动时的高不稳定度。
riskCounter += SCAN_INTERVAL;
if (riskCounter >= RISK_INTERVAL || !cachedRisk.found() || craftingChanged) {
riskCounter = 0;
if (level.getBlockEntity(found) instanceof thaumcraft.common.blockentities.InfusionMatrixBlockEntity matrix) {
newRisk = InfusionRiskCalculator.computeRisk(level,
new InfusionRiskCalculator.InfusionMatrixInfo(found, matrix));
} else {
newRisk = RiskData.NONE;
}
}
} else {
newRisk = RiskData.NONE;
cachedCrafting = false;
}
boolean riskChanged = !newRisk.equals(this.cachedRisk);
this.cachedRisk = newRisk;
if (riskChanged || !java.util.Objects.equals(found, this.matrixPos)) {
this.matrixPos = found;
markForUpdate();
}
}
// ===== 网格节点 / 状态同步 =====
private void updateNetworkState() {
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean net = mainNode.isActive();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.NETWORK) != net) {
level.setBlock(worldPosition, st.setValue(ModBlocks.NETWORK, net), 3);
// setBlock 后本 BE 仍存在;刷新引用
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
private void updateBookState() {
if (level == null || level.isClientSide() || unloading || isRemoved()) return;
boolean hasBook = !inventory.getItem(BOOK_SLOT).isEmpty();
BlockState st = getBlockState();
if (st.getValue(ModBlocks.BOOK) != hasBook) {
level.setBlock(worldPosition, st.setValue(ModBlocks.BOOK, hasBook), 3);
BlockEntity be = level.getBlockEntity(worldPosition);
if (be != null && be != this) {
be.setChanged();
}
}
}
// ===== 悬浮气泡(text_display 实体) =====
/**
* 每 tick 更新气泡:条件 = 放入魔导手册 且 连接 AE 网络 且 检测到注魔祭坛。
* 满足 → 生成/更新气泡;不满足 → 销毁气泡。
*/
private void updateBubble() {
if (level == null || level.isClientSide() || unloading) return;
boolean shouldShow = getBlockState().getValue(ModBlocks.BOOK)
&& getBlockState().getValue(ModBlocks.NETWORK)
&& cachedRisk.found();
if (shouldShow) {
ensureBubble();
if (bubble != null) {
// 文本未变则跳过反射/NBT 重建(原实现每 tick 都重建,是 mspt 开销之一)
net.minecraft.network.chat.Component text = buildBubbleText(cachedRisk);
if (!text.equals(lastBubbleText)) {
lastBubbleText = text;
applyBubbleNbt(text);
}
}
} else {
discardBubble();
lastBubbleText = null;
}
}
/** 确保气泡实体存在(不存在则生成;区块重载后按 Tag 找回旧实体)。 */
private void ensureBubble() {
if (bubble != null && bubble.isAlive()) return;
bubble = findBubble();
if (bubble != null) return;
bubble = EntityType.TEXT_DISPLAY.create(level);
if (bubble == null) return;
bubble.setPos(worldPosition.getX() + 0.5, worldPosition.getY() + 1.2, worldPosition.getZ() + 0.5);
// 1.21.1 TextDisplay 配置 API 均为 private/protected:反射调用 readAdditionalSaveData
// 用 summon 命令同款 NBT 一次配置全部字段(text/background/billboard/shadow/see_through 等)。
applyBubbleNbt(buildBubbleText(cachedRisk));
bubble.addTag(BUBBLE_TAG);
level.addFreshEntity(bubble);
}
/** 反射调用 readAdditionalSaveData(CompoundTag) 配置气泡(缓存 Method)。 */
private static java.lang.reflect.Method READ_NBT_METHOD;
private void applyBubbleNbt(Component text) {
if (bubble == null || level == null) return;
try {
if (READ_NBT_METHOD == null) {
READ_NBT_METHOD = net.minecraft.world.entity.Entity.class
.getDeclaredMethod("readAdditionalSaveData", CompoundTag.class);
READ_NBT_METHOD.setAccessible(true);
}
CompoundTag nbt = new CompoundTag();
nbt.putString("text", Component.Serializer.toJson(text, level.registryAccess()));
nbt.putBoolean("default_background", false);
nbt.putInt("background", BUBBLE_BG_COLOR);
nbt.putString("billboard", "vertical");
nbt.putInt("line_width", BUBBLE_LINE_WIDTH);
nbt.putBoolean("shadow", true);
nbt.putBoolean("see_through", false);
// 必须写 alignment:TextDisplay.readAdditionalSaveData 无条件 decode "alignment",
// 缺失时 tag.get 返回 null → Align.CODEC(字符串) decode 失败 → 服务端疯狂刷 "Display entityNot a string"。
nbt.putString("alignment", "center");
// 文字调小:transformation.scale 整体缩到 0.5(text_display 无字号参数,靠实体缩放)
CompoundTag transform = new CompoundTag();
transform.put("scale", floatList(0.5F, 0.5F, 0.5F));
transform.put("translation", floatList(0F, 0F, 0F));
transform.put("left_rotation", floatList(0F, 0F, 0F, 1F));
transform.put("right_rotation", floatList(0F, 0F, 0F, 1F));
nbt.put("transformation", transform);
READ_NBT_METHOD.invoke(bubble, nbt);
} catch (Exception e) {
ThaumicEnergistics.LOG.warn("[Monitor] 气泡配置失败: {}", e.toString());
}
}
/** 构造 FloatTag 列表(1.21.1 CompoundTag 无 putFloatArray,需 ListTag)。 */
private static net.minecraft.nbt.ListTag floatList(float... values) {
net.minecraft.nbt.ListTag list = new net.minecraft.nbt.ListTag();
for (float v : values) {
list.add(net.minecraft.nbt.FloatTag.valueOf(v));
}
return list;
}
/** 按 Tag + 方块附近 AABB 查找已存在的气泡。 */
private net.minecraft.world.entity.Display.TextDisplay findBubble() {
if (level == null) return null;
AABB aabb = new AABB(worldPosition).inflate(0.15, 2.0, 0.15);
List<net.minecraft.world.entity.Display.TextDisplay> list = level.getEntitiesOfClass(
net.minecraft.world.entity.Display.TextDisplay.class, aabb,
e -> e.getTags().contains(BUBBLE_TAG));
return list.isEmpty() ? null : list.get(0);
}
/** 销毁气泡(方块拆除/条件不满足时)。 */
private void discardBubble() {
if (bubble != null && bubble.isAlive()) {
bubble.discard();
} else {
net.minecraft.world.entity.Display.TextDisplay found = findBubble();
if (found != null) {
found.discard();
}
}
bubble = null;
}
/** 风险数据 → 气泡多行文本(2 行:注魔风险/不稳定度;不显示基础+祭坛细分,细分在聊天栏)。 */
private static Component buildBubbleText(RiskData risk) {
// 内边距必须用不间断空格(\u00A0):普通空格在行尾会被字体布局裁剪,
// 导致背景只包住左侧、右侧贴边。NBSP 有真实字形,两侧都保留。
String pad = "\u00A0\u00A0"; // 左右内边距(NBSP)
String tierWord = tierWord(risk.tier());
Component line1 = Component.literal(pad + "注魔风险: " + tierWord + pad)
.withStyle(style -> style.withColor(tierColor(risk.tier())));
Component line2 = Component.literal(pad + "不稳定度: " + risk.instability() + pad)
.withStyle(style -> style.withColor(0xFFFFFFFF));
// 上下各留一个空行(垂直内边距)
return Component.literal("\n")
.append(line1).append("\n")
.append(line2).append("\n");
}
/** 5 档文字(无星星)。 */
private static String tierWord(int tier) {
return switch (tier) {
case 1 -> "极低";
case 2 -> "低";
case 3 -> "中";
case 4 -> "高";
case 5 -> "极高";
default -> "?";
};
}
/** 5 档描述(神秘时代风)。 */
private static String tierDesc(int tier) {
return switch (tier) {
case 1 -> "灵光如镜,仪式安然";
case 2 -> "灵光微澜,无碍仪式";
case 3 -> "符文低鸣,灵光流转不宁";
case 4 -> "裂隙于虚空中睁开,注视着祭坛";
case 5 -> "咒波翻涌,裂隙将吞没一切";
default -> "";
};
}
/** 档位颜色:1 绿 → 5 红。 */
private static int tierColor(int tier) {
return switch (tier) {
case 1 -> 0xFF55FF55;
case 2 -> 0xFFAAFF55;
case 3 -> 0xFFFFFF55;
case 4 -> 0xFFFFAA55;
default -> 0xFFFF5555;
};
}
/** 放入魔导手册(方块右键拦截后调用,防止书自身 use 打开界面)。 */
public void placeBook(Player player, ItemStack held) {
if (level == null || level.isClientSide()) return;
if (inventory.getItem(BOOK_SLOT).isEmpty()) {
inventory.setItem(BOOK_SLOT, held.copyWithCount(1));
if (!player.getAbilities().instabuild) {
held.shrink(1);
}
setChanged();
}
}
/** 取回魔导手册(Shift+右键)。 */
public void retrieveBook(Player player) {
if (level == null || level.isClientSide()) return;
ItemStack book = inventory.getItem(BOOK_SLOT);
if (!book.isEmpty()) {
if (!player.getInventory().add(book)) {
net.minecraft.world.Containers.dropItemStack(level, worldPosition.getX(), worldPosition.getY(), worldPosition.getZ(), book);
}
inventory.setItem(BOOK_SLOT, ItemStack.EMPTY);
setChanged();
}
}
/** 右键:聊天框提示。需手册 + 联网才显示详细风险;否则提示缺失项。 */
public void showDescriptionInChat(Player player) {
if (level == null || level.isClientSide()) return;
boolean hasBook = getBlockState().getValue(ModBlocks.BOOK);
boolean hasNetwork = getBlockState().getValue(ModBlocks.NETWORK);
if (!hasBook && !hasNetwork) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 缺少引导的书籍与网络能量(魔导手册与 AE 网络)"));
return;
}
if (!hasBook) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 需要放置魔导手册作为引导"));
return;
}
if (!hasNetwork) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 需要接入 AE 网络供能"));
return;
}
if (!cachedRisk.found()) {
player.sendSystemMessage(Component.literal("§7韵魔观测器: 未检测到注魔祭坛"));
return;
}
String tierWord = tierWord(cachedRisk.tier());
String desc = tierDesc(cachedRisk.tier());
Component msg = Component.literal("注魔风险: ")
.withStyle(style -> style.withColor(tierColor(cachedRisk.tier())))
.append(Component.literal(tierWord + "§7"))
.append(Component.literal("\n不稳定度: " + cachedRisk.instability()
+ " (基础 " + cachedRisk.recipeInstability() + " + 祭坛 " + cachedRisk.symmetry() + ")§f\n"))
.append(Component.literal(desc));
player.sendSystemMessage(msg);
}
public static boolean isThaumonomicon(ItemStack stack) {
if (stack.isEmpty()) return false;
return stack.is(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
net.minecraft.resources.ResourceLocation.fromNamespaceAndPath("thaumcraft", "thaumonomicon")));
}
// ===== 访问器 =====
public Container getInventory() {
return inventory;
}
public boolean hasBook() {
return !inventory.getItem(BOOK_SLOT).isEmpty();
}
public RiskData getCachedRisk() {
return cachedRisk;
}
/** ME 网络是否在线(节点 active:有能量 + 有频道)。Jade 显示网络状态用。 */
public boolean isNetworkOnline() {
return mainNode.isActive();
}
/** 网格节点是否有能量。Jade 状态细分(在线/无频道/离线)用。 */
public boolean isNodePowered() {
return mainNode.isPowered();
}
// ===== IInWorldGridNodeHost =====
@Override
public IGridNode getGridNode(Direction dir) {
return mainNode.getNode();
}
public AECableType getCableConnectionType(Direction dir) {
return AECableType.SMART;
}
public void markForUpdate() {
// 卸载/移除中跳过:sendBlockUpdated / blockEntityChanged 会把区块重新标记脏,停服保存时形成死循环
if (level == null || unloading || isRemoved()) return;
setChanged();
level.sendBlockUpdated(worldPosition, getBlockState(), getBlockState(), 3);
// blockEntityChanged → 触发 getUpdateTag 同步到客户端(悬浮弹窗数据依赖)
level.blockEntityChanged(worldPosition);
}
// ===== NBT =====
@Override
public CompoundTag getUpdateTag(HolderLookup.Provider reg) {
CompoundTag tag = super.getUpdateTag(reg);
saveRiskToTag(tag);
if (!inventory.getItem(BOOK_SLOT).isEmpty()) {
tag.put("Book", inventory.getItem(BOOK_SLOT).save(reg));
}
return tag;
}
@Override
public void handleUpdateTag(CompoundTag tag, HolderLookup.Provider reg) {
super.handleUpdateTag(tag, reg);
loadRiskFromTag(tag);
if (tag.contains("Book")) {
inventory.setItem(BOOK_SLOT, ItemStack.parseOptional(reg, tag.getCompound("Book")));
}
}
private void saveRiskToTag(CompoundTag tag) {
tag.putBoolean("RiskFound", cachedRisk.found());
tag.putBoolean("RiskCrafting", cachedRisk.crafting());
tag.putInt("RiskTier", cachedRisk.tier());
tag.putInt("RiskInstability", cachedRisk.instability());
tag.putInt("RiskSymmetry", cachedRisk.symmetry());
tag.putInt("RiskRecipeInstability", cachedRisk.recipeInstability());
tag.putString("RiskTierText", cachedRisk.tierText());
tag.putString("RiskShortages", cachedRisk.shortages());
tag.putInt("RiskTotalNeeded", cachedRisk.totalNeeded());
}
private void loadRiskFromTag(CompoundTag tag) {
this.cachedRisk = new RiskData(
tag.getBoolean("RiskFound"),
tag.getBoolean("RiskCrafting"),
tag.getInt("RiskTier"),
tag.getInt("RiskInstability"),
tag.getInt("RiskSymmetry"),
tag.getInt("RiskRecipeInstability"),
tag.getString("RiskTierText"),
tag.getString("RiskShortages"),
tag.getInt("RiskTotalNeeded"));
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider reg) {
super.loadAdditional(tag, reg);
if (tag.contains("Book")) {
inventory.setItem(BOOK_SLOT, ItemStack.parseOptional(reg, tag.getCompound("Book")));
}
if (tag.contains("MatrixPos")) {
matrixPos = BlockPos.of(tag.getLong("MatrixPos"));
}
mainNode.loadFromNBT(tag);
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider reg) {
super.saveAdditional(tag, reg);
if (!inventory.getItem(BOOK_SLOT).isEmpty()) {
tag.put("Book", inventory.getItem(BOOK_SLOT).save(reg));
}
if (matrixPos != null) {
tag.putLong("MatrixPos", matrixPos.asLong());
}
mainNode.saveToNBT(tag);
}
/** 注魔风险缓存数据(Jade 用)。 */
public record RiskData(boolean found, boolean crafting, int tier, int instability, int symmetry,
int recipeInstability, String tierText, String shortages, int totalNeeded) {
public static final RiskData NONE = new RiskData(false, false, 0, 0, 0, 0, "", "", 0);
}
}
@@ -1,10 +1,7 @@
package thaumicenergistics.config; package thaumicenergistics.config;
import java.util.Set;
public final class ThEConfig { public final class ThEConfig {
private ThEConfig() {} private ThEConfig() {}
public static final double EVC_POWER_PER_TICK = 5.0; public static final double EVC_POWER_PER_TICK = 5.0;
public static final int CELL_1K = 1024, CELL_4K = 4096, CELL_16K = 16384, CELL_64K = 65536; public static final int CELL_1K = 1024, CELL_4K = 4096, CELL_16K = 16384, CELL_64K = 65536;
public static final int CELL_MAX_TYPES = 12; public static final int CELL_MAX_TYPES = 12;
private static final Set<String> ENABLED = Set.of("cells","io_buses","monitoring","arcane_crafting_terminal","essentia_provider","infusion_provider","vibration_chamber","vis_interface","golem_backpack");
public static boolean isFeatureEnabled(String n) { return ENABLED.contains(n); }
} }
@@ -41,6 +41,14 @@ public final class ModBlockEntities {
REGISTRY.register("essentia_cell_workbench", REGISTRY.register("essentia_cell_workbench",
() -> BlockEntityType.Builder.of(TileEssentiaCellWorkbench::new, ModBlocks.ESSENTIA_CELL_WORKBENCH.get()).build(null)); () -> BlockEntityType.Builder.of(TileEssentiaCellWorkbench::new, ModBlocks.ESSENTIA_CELL_WORKBENCH.get()).build(null));
public static final Supplier<BlockEntityType<TileInfusionMonitor>> INFUSION_MONITOR =
REGISTRY.register("infusion_monitor",
() -> BlockEntityType.Builder.of(TileInfusionMonitor::new, ModBlocks.INFUSION_MONITOR.get()).build(null));
public static final Supplier<BlockEntityType<TileEssentiaProviderConnection>> ESSENTIA_PROVIDER_CONNECTION =
REGISTRY.register("essentia_provider_connection",
() -> BlockEntityType.Builder.of(TileEssentiaProviderConnection::new, ModBlocks.ESSENTIA_PROVIDER_CONNECTION.get()).build(null));
/** /**
* 虚拟的 Thaumcraft vis 源,供 vis 接口的提供端使用。 * 虚拟的 Thaumcraft vis 源,供 vis 接口的提供端使用。
* 不作为真实方块放置;由 {@link thaumicenergistics.common.parts.VisInterfacePart} * 不作为真实方块放置;由 {@link thaumicenergistics.common.parts.VisInterfacePart}
@@ -8,12 +8,14 @@ import net.minecraft.world.level.block.SoundType;
import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.MapColor; import net.minecraft.world.level.material.MapColor;
import net.minecraft.world.item.ItemStack;
import net.neoforged.neoforge.registries.DeferredBlock; import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredRegister; import net.neoforged.neoforge.registries.DeferredRegister;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.blocks.OrientableEntityBlock; import thaumicenergistics.common.blocks.OrientableEntityBlock;
import thaumicenergistics.common.blocks.ThEBaseEntityBlock; import thaumicenergistics.common.blocks.ThEBaseEntityBlock;
import thaumicenergistics.common.tiles.*; import thaumicenergistics.common.tiles.*;
import thaumicenergistics.init.ModItems;
import net.minecraft.network.chat.Component; import net.minecraft.network.chat.Component;
public final class ModBlocks { public final class ModBlocks {
@@ -119,4 +121,240 @@ public final class ModBlocks {
return null; return null;
} }
}); });
// ==================== 注魔监视器(灵气观测器) ====================
/** 是否放入魔导手册。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty BOOK =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("book");
/** 是否连接 AE 网络。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty NETWORK =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("network");
/**
* 注魔监视器:4 态(off / off+book / off+book+on / off+Jade),blockstate 用 multipart 合并模型。
*/
public static final DeferredBlock<Block> INFUSION_MONITOR = REGISTRY.register("infusion_monitor", () -> new ThEBaseEntityBlock(P) {
@Override
public net.minecraft.world.level.block.entity.BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new TileInfusionMonitor(pos, state);
}
@Override
public <T extends net.minecraft.world.level.block.entity.BlockEntity> net.minecraft.world.level.block.entity.BlockEntityTicker<T> getTicker(net.minecraft.world.level.Level level, BlockState state, net.minecraft.world.level.block.entity.BlockEntityType<T> type) {
if (level.isClientSide()) return null;
if (type == ModBlockEntities.INFUSION_MONITOR.get()) {
return (lvl, pos, st, be) -> TileInfusionMonitor.tick(lvl, pos, st, (TileInfusionMonitor) be);
}
return null;
}
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(BOOK, NETWORK, FACING);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 放置时始终朝向放置者
return this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite())
.setValue(BOOK, false)
.setValue(NETWORK, false);
}
@Override
protected net.minecraft.world.ItemInteractionResult useItemOn(net.minecraft.world.item.ItemStack stack, BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) {
if (level.getBlockEntity(pos) instanceof TileInfusionMonitor monitor) {
// 手持魔导手册 → 直接放入(返回 SUCCESS 拦截,避免魔导手册自身 use 打开界面)
if (TileInfusionMonitor.isThaumonomicon(stack)) {
if (!level.isClientSide) {
monitor.placeBook(player, stack);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// Shift+右键(非手册物品)→ 取回手册
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
monitor.retrieveBook(player);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 普通右键(非手册/非Shift)→ 聊天框显示风险描述
if (!level.isClientSide) {
monitor.showDescriptionInChat(player);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
return net.minecraft.world.ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION;
}
// 空手右键(1.21 走 useWithoutItem):Shift+空手 → 取回手册;普通空手右键 → 聊天框显示描述
@Override
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.phys.BlockHitResult hit) {
if (level.getBlockEntity(pos) instanceof TileInfusionMonitor monitor) {
if (player.isShiftKeyDown()) {
if (!level.isClientSide) {
monitor.retrieveBook(player);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
if (!level.isClientSide) {
monitor.showDescriptionInChat(player);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
return net.minecraft.world.InteractionResult.PASS;
}
@Override
public net.minecraft.world.phys.shapes.VoxelShape getShape(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
// 底座(高 0.25)+ 上部塔架(0.25~0.8)
return net.minecraft.world.phys.shapes.Shapes.or(
net.minecraft.world.phys.shapes.Shapes.box(0.0, 0.0, 0.0, 1.0, 0.25, 1.0),
net.minecraft.world.phys.shapes.Shapes.box(0.15, 0.25, 0.15, 0.85, 0.8, 0.85));
}
});
// ==================== Alkusure86 玩偶(Fumo) ====================
/** 水平朝向(韵魔观测器共用)。 */
public static final net.minecraft.world.level.block.state.properties.DirectionProperty FACING =
net.minecraft.world.level.block.state.properties.BlockStateProperties.HORIZONTAL_FACING;
// 姿态变体:0 = alkusure86fumo(默认坐姿),1 = a999csfumo。
public static final net.minecraft.world.level.block.state.properties.BooleanProperty VARIANT =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("variant");
// 玩偶专用:低硬度 → 空手可快速挖掘(装饰物)。
private static final BlockBehaviour.Properties PD =
BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_PINK).strength(0.2f).sound(SoundType.WOOL);
// Alkusure86 玩偶:纯装饰方块,2 个姿态模型 + 水平朝向。
// 右键切换姿态;Shift+右键拿起方块到手。
public static final DeferredBlock<Block> ALKUSURE86_FUMO = REGISTRY.register("alkusure86fumo", () -> new Block(PD) {
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, VARIANT);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 默认坐姿(variant=0),面向放置者
return this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite())
.setValue(VARIANT, false);
}
@Override
protected net.minecraft.world.ItemInteractionResult useItemOn(net.minecraft.world.item.ItemStack stack, BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.InteractionHand hand, net.minecraft.world.phys.BlockHitResult hit) {
if (player.isShiftKeyDown()) {
// Shift+右键:拿起方块到手(或背包)
pickUp(level, pos, player);
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 右键:切换姿态
if (!level.isClientSide) {
level.setBlock(pos, state.setValue(VARIANT, !state.getValue(VARIANT)), 3);
}
return net.minecraft.world.ItemInteractionResult.sidedSuccess(level.isClientSide());
}
// 空手右键(1.21 走 useWithoutItem):Shift+空手拿起;右键切换姿态
@Override
protected net.minecraft.world.InteractionResult useWithoutItem(BlockState state, net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player, net.minecraft.world.phys.BlockHitResult hit) {
if (player.isShiftKeyDown()) {
pickUp(level, pos, player);
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
if (!level.isClientSide) {
level.setBlock(pos, state.setValue(VARIANT, !state.getValue(VARIANT)), 3);
}
return net.minecraft.world.InteractionResult.sidedSuccess(level.isClientSide());
}
// Shift+右键拿起:移除方块,物品进主手(空手时)或背包。
private void pickUp(net.minecraft.world.level.Level level, BlockPos pos, net.minecraft.world.entity.player.Player player) {
if (level.isClientSide) return;
ItemStack item = new ItemStack(ModItems.ALKUSURE86_FUMO_ITEM.get());
if (player.getMainHandItem().isEmpty()) {
player.setItemInHand(net.minecraft.world.InteractionHand.MAIN_HAND, item);
} else if (!player.getInventory().add(item)) {
net.minecraft.world.Containers.dropItemStack(level, pos.getX() + 0.5, pos.getY() + 0.5, pos.getZ() + 0.5, item);
}
level.removeBlock(pos, false);
}
@Override
public net.minecraft.world.phys.shapes.VoxelShape getShape(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
// 玩偶整体占位:坐姿高度约 12 格单位(0.75 格),宽约 8(0.5 格)
return net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.0, 0.25, 0.75, 0.75, 0.75);
}
// 纯 Block 无战利品表 → 必须覆写 getDrops,否则挖掘不掉落
@Override
public java.util.List<ItemStack> getDrops(BlockState state, net.minecraft.world.level.storage.loot.LootParams.Builder builder) {
return java.util.List.of(new ItemStack(ModItems.ALKUSURE86_FUMO_ITEM.get()));
}
});
// ==================== 源质无线接收器 ====================
/** 是否已连接源质供应器(off/on 模型切换)。 */
public static final net.minecraft.world.level.block.state.properties.BooleanProperty CONNECTED =
net.minecraft.world.level.block.state.properties.BooleanProperty.create("connected");
/** 接收器六向贴放(贴墙/立地/挂顶)。 */
public static final net.minecraft.world.level.block.state.properties.DirectionProperty FACING_ALL =
net.minecraft.world.level.block.state.properties.BlockStateProperties.FACING;
/** 接收器专用:低硬度 → 镐子可快速挖掘。 */
private static final BlockBehaviour.Properties PC =
BlockBehaviour.Properties.of().mapColor(MapColor.STONE).strength(1.0f).sound(SoundType.STONE);
public static final DeferredBlock<Block> ESSENTIA_PROVIDER_CONNECTION = REGISTRY.register("essentia_provider_connection", () -> new ThEBaseEntityBlock(PC) {
@Override
public net.minecraft.world.level.block.entity.BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new TileEssentiaProviderConnection(pos, state);
}
@Override
public <T extends net.minecraft.world.level.block.entity.BlockEntity> net.minecraft.world.level.block.entity.BlockEntityTicker<T> getTicker(net.minecraft.world.level.Level level, BlockState state, net.minecraft.world.level.block.entity.BlockEntityType<T> type) {
if (!level.isClientSide() && type == ModBlockEntities.ESSENTIA_PROVIDER_CONNECTION.get()) {
return (lvl, pos, st, be) -> TileEssentiaProviderConnection.serverTick(lvl, pos, st, (TileEssentiaProviderConnection) be);
}
return null;
}
@Override
protected void createBlockStateDefinition(net.minecraft.world.level.block.state.StateDefinition.Builder<Block, BlockState> builder) {
builder.add(CONNECTED, FACING_ALL);
}
@Override
public BlockState getStateForPlacement(net.minecraft.world.item.context.BlockPlaceContext context) {
// 正面朝外 = 点击面的方向(贴墙/立地/挂顶)
return this.defaultBlockState()
.setValue(FACING_ALL, context.getClickedFace())
.setValue(CONNECTED, false);
}
@Override
public net.minecraft.world.phys.shapes.VoxelShape getShape(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) {
// 薄碰撞箱(贴墙/立地/挂顶),不遮挡光线;UP=立地(底部) DOWN=挂顶(顶部)
return switch (state.getValue(FACING_ALL)) {
case NORTH -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.25, 0.75, 0.75, 0.75, 1.0);
case SOUTH -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.25, 0.0, 0.75, 0.75, 0.25);
case EAST -> net.minecraft.world.phys.shapes.Shapes.box(0.0, 0.25, 0.25, 0.25, 0.75, 0.75);
case WEST -> net.minecraft.world.phys.shapes.Shapes.box(0.75, 0.25, 0.25, 1.0, 0.75, 0.75);
case UP -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.0, 0.25, 0.75, 0.25, 0.75); // 立地:碰撞箱在底部
case DOWN -> net.minecraft.world.phys.shapes.Shapes.box(0.25, 0.75, 0.25, 0.75, 1.0, 0.75); // 挂顶:碰撞箱在顶部
};
}
@Override
public int getLightBlock(BlockState state, net.minecraft.world.level.BlockGetter level, BlockPos pos) {
return 0; // 不遮挡光线
}
});
} }
@@ -38,6 +38,10 @@ public final class ModCreativeTab {
output.accept(ModItems.ARCANE_ASSEMBLER_ITEM.get()); output.accept(ModItems.ESSENTIA_VIBRATION_CHAMBER_ITEM.get()); output.accept(ModItems.ARCANE_ASSEMBLER_ITEM.get()); output.accept(ModItems.ESSENTIA_VIBRATION_CHAMBER_ITEM.get());
output.accept(ModItems.KNOWLEDGE_INSCRIBER_ITEM.get()); output.accept(ModItems.ESSENTIA_CELL_WORKBENCH_ITEM.get()); output.accept(ModItems.KNOWLEDGE_INSCRIBER_ITEM.get()); output.accept(ModItems.ESSENTIA_CELL_WORKBENCH_ITEM.get());
output.accept(ModItems.DISTILLATION_ENCODER_ITEM.get()); output.accept(ModItems.DISTILLATION_ENCODER_ITEM.get());
output.accept(ModItems.INFUSION_MONITOR_ITEM.get());
output.accept(ModItems.ESSENTIA_PROVIDER_CONNECTION_ITEM.get()); output.accept(ModItems.WIRELESS_CONNECTOR.get());
// 玩偶不进创造物品栏:需 /give @p thaumicenergistics:alkusure86fumo 手动获取
// output.accept(ModItems.ALKUSURE86_FUMO_ITEM.get());
// 源质虚拟物品不再在创造标签页中注册(避免玩家拿到物品栏) // 源质虚拟物品不再在创造标签页中注册(避免玩家拿到物品栏)
// 它们通过 JEI 插件注册,仅作为 ghost ingredient 可拖拽到 AE 筛选槽 // 它们通过 JEI 插件注册,仅作为 ghost ingredient 可拖拽到 AE 筛选槽
@@ -78,4 +78,13 @@ public final class ModItems {
public static final DeferredItem<BlockItem> KNOWLEDGE_INSCRIBER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.KNOWLEDGE_INSCRIBER); public static final DeferredItem<BlockItem> KNOWLEDGE_INSCRIBER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.KNOWLEDGE_INSCRIBER);
public static final DeferredItem<BlockItem> ESSENTIA_CELL_WORKBENCH_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ESSENTIA_CELL_WORKBENCH); public static final DeferredItem<BlockItem> ESSENTIA_CELL_WORKBENCH_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ESSENTIA_CELL_WORKBENCH);
public static final DeferredItem<BlockItem> DISTILLATION_ENCODER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.DISTILLATION_ENCODER); public static final DeferredItem<BlockItem> DISTILLATION_ENCODER_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.DISTILLATION_ENCODER);
public static final DeferredItem<BlockItem> INFUSION_MONITOR_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.INFUSION_MONITOR);
public static final DeferredItem<BlockItem> ALKUSURE86_FUMO_ITEM = REGISTRY.registerSimpleBlockItem(ModBlocks.ALKUSURE86_FUMO);
public static final DeferredItem<BlockItem> ESSENTIA_PROVIDER_CONNECTION_ITEM =
REGISTRY.registerSimpleBlockItem(ModBlocks.ESSENTIA_PROVIDER_CONNECTION);
/** 无线连接器:右键接收器选中,再右键源质供应器建立连接。 */
public static final DeferredItem<net.minecraft.world.item.Item> WIRELESS_CONNECTOR =
REGISTRY.register("wireless_connector",
() -> new thaumicenergistics.common.items.WirelessConnectorItem(new net.minecraft.world.item.Item.Properties()));
} }
@@ -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")));
} }
@@ -22,7 +22,8 @@ public class AspectIngredientHelper implements IIngredientHelper<AspectIngredien
@Override @Override
public String getDisplayName(AspectIngredient ingredient) { public String getDisplayName(AspectIngredient ingredient) {
if (ingredient == null || ingredient.aspect() == null) return "Unknown"; if (ingredient == null || ingredient.aspect() == null) return "Unknown";
return ingredient.aspect().getName(); // 用本地化显示名(displayName,中文环境显示中文),与 AE 终端内源质名称一致
return ingredient.aspect().displayName().getString();
} }
@Override @Override
@@ -33,7 +33,8 @@ public class AspectIngredientRenderer implements IIngredientRenderer<AspectIngre
return tooltip; return tooltip;
} }
Aspect aspect = ingredient.aspect(); Aspect aspect = ingredient.aspect();
tooltip.add(Component.literal(aspect.getName())); // 本地化显示名(中文环境显示中文),与 AE 终端内源质名称一致
tooltip.add(aspect.displayName());
tooltip.add(Component.literal(aspect.getTag()).withStyle(style -> style.withColor(0x888888))); tooltip.add(Component.literal(aspect.getTag()).withStyle(style -> style.withColor(0x888888)));
return tooltip; return tooltip;
} }
@@ -0,0 +1,87 @@
package thaumicenergistics.integration.jei;
import java.util.List;
import java.util.Optional;
import net.minecraft.client.renderer.Rect2i;
import mezz.jei.api.gui.builder.IClickableIngredientFactory;
import mezz.jei.api.gui.handlers.IGuiContainerHandler;
import mezz.jei.api.runtime.IClickableIngredient;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.StackWithBounds;
import appeng.core.definitions.AEItems;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
/**
* AE 界面(源质终端等)的 JEI GuiContainerHandler:
* 悬停仓库中的源质条目时,返回 {@link AspectIngredient} 作为可点击成分,
* 使 JEI 的 A 键收藏/配方查看能正确显示源质图标与名称。
* <p>
* 背景:源质是非物品/流体 AEKey,repo 中显示为 WrappedGenericStack 物品。
* 若不提供转换,JEI 收藏会退化为"包裹通用堆栈"(无图标、名字丢失)。
* 本处理不依赖 AE2-JEI-Integration 是否存在,ThE 自带即可生效。
*/
public class ThEEssentiaGuiHandler implements IGuiContainerHandler<AEBaseScreen<?>> {
@Override
public Optional<? extends IClickableIngredient<?>> getClickableIngredientUnderMouse(
IClickableIngredientFactory factory, AEBaseScreen<?> screen, double mouseX, double mouseY) {
StackWithBounds stackWithBounds = screen.getStackUnderMouse(mouseX, mouseY);
if (stackWithBounds == null) {
return Optional.empty();
}
AEssentiaKey key = unwrapEssentiaKey(stackWithBounds.stack());
if (key == null) {
return Optional.empty();
}
// AEssentiaKey id = thaumcraft:<tag> → Aspect
Aspect aspect = Aspect.getAspect(key.getId().getPath());
if (aspect == null) {
return Optional.empty();
}
return factory.createBuilder(AspectIngredientType.INSTANCE, new AspectIngredient(aspect))
.buildWithArea(stackWithBounds.bounds());
}
@Override
public List<Rect2i> getGuiExtraAreas(AEBaseScreen<?> screen) {
return List.of();
}
/**
* 从 GenericStack 中解析源质 key,兼容两种形式:
* <ul>
* <li>直接 {@link AEssentiaKey}(StackWithBounds.fromSlot 已 unwrap 的形式)</li>
* <li>{@code WrappedGenericStack} 物品包裹的形式(unwrap 失败/直接拿到物品的形式)</li>
* </ul>
*/
private static AEssentiaKey unwrapEssentiaKey(GenericStack stack) {
if (stack == null) {
return null;
}
AEKey what = stack.what();
if (what instanceof AEssentiaKey essentiaKey) {
return essentiaKey;
}
if (what instanceof AEItemKey itemKey) {
var itemStack = itemKey.toStack();
if (AEItems.WRAPPED_GENERIC_STACK.is(itemStack)) {
var unwrapped = GenericStack.unwrapItemStack(itemStack);
if (unwrapped != null && unwrapped.what() instanceof AEssentiaKey wrappedKey) {
return wrappedKey;
}
}
}
return null;
}
}
@@ -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());
@@ -99,6 +97,12 @@ public class ThEJeiPlugin implements IModPlugin {
@Override @Override
public void registerGuiHandlers(IGuiHandlerRegistration registration) { public void registerGuiHandlers(IGuiHandlerRegistration registration) {
// 源质终端(含无线版):悬停仓库源质 → 提供 AspectIngredient 可点击项,
// 使 JEI 的 A 键收藏/配方查看正确显示源质图标与名称(不依赖 AE2-JEI-Integration)。
registration.addGuiContainerHandler(
thaumicenergistics.common.container.GuiEssentiaTerminal.class,
new ThEEssentiaGuiHandler());
// 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能) // 源质元件工作台:从 JEI 拖拽要素到分区格添加分区(纯 JEI API,可选功能)
registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler()); registration.addGhostIngredientHandler(GuiEssentiaCellWorkbench.class, new CellWorkbenchGhostIngredientHandler());
// 知识记录仪:从 JEI 拉取物品到 3×3 合成网格(复用 GhostSlot + KnowledgeInscriberGhostSlotPacket) // 知识记录仪:从 JEI 拉取物品到 3×3 合成网格(复用 GhostSlot + KnowledgeInscriberGhostSlotPacket)
@@ -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;
@@ -20,14 +22,20 @@ import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket; import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import appeng.api.stacks.AEItemKey;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.neoforged.neoforge.network.PacketDistributor; 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,33 +73,70 @@ 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;
} }
// 构建模板物品列表(9 格,对应 3x3 合成格) // 构建模板物品列表(9 格,对应 3x3 合成格)——按可用变体挑选:
// 奥术配方 recipeId=null,服务端只能用客户端模板重建 Ingredient,
// 固定取 variants.get(0)(如石头)会导致有深板岩/圆石却拉不到。
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY); var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var inputs = recipe.inputs(); var inputs = recipe.inputs();
var stored = menu.snapshotStoredAmounts();
var craftable = menu.snapshotCraftableKeys();
var reserved = new Object2IntOpenHashMap<AEItemKey>();
for (int i = 0; i < templateItems.size() && i < inputs.size(); i++) { for (int i = 0; i < templateItems.size() && i < inputs.size(); i++) {
var variants = inputs.get(i); var variants = inputs.get(i);
if (variants != null && !variants.isEmpty()) { if (variants != null && !variants.isEmpty()) {
templateItems.set(i, variants.get(0).copyWithCount(1)); Ingredient ingredient = Ingredient.of(variants.stream());
templateItems.set(i, menu.pickAvailableTemplate(ingredient, stored, craftable, reserved));
} }
} }
// 奥术配方不在 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;
}
} }
@@ -26,7 +26,9 @@ import mezz.jei.api.recipe.transfer.IRecipeTransferHandlerHelper;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket; import appeng.core.network.serverbound.FillCraftingGridFromRecipePacket;
import appeng.api.stacks.AEItemKey;
import appeng.util.CraftingRecipeUtil; import appeng.util.CraftingRecipeUtil;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.world.inventory.MenuType; import net.minecraft.world.inventory.MenuType;
import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.ThaumicEnergistics;
@@ -82,8 +84,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 +96,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,22 +117,25 @@ 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;
} }
// 实际转移:发送 ME 网络填充包 // 实际转移:发送 ME 网络填充包
// 模板按可用变体挑选(网络有货→背包有货→可合成→首个),
// 否则固定取 items[0](如石头),网络里只有深板岩/圆石时槽位会拉不到东西。
var templateItems = NonNullList.withSize(9, ItemStack.EMPTY); var templateItems = NonNullList.withSize(9, ItemStack.EMPTY);
var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe); var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe);
var stored = menu.snapshotStoredAmounts();
var craftable = menu.snapshotCraftableKeys();
var reserved = new Object2IntOpenHashMap<AEItemKey>();
for (int i = 0; i < ingredients.size() && i < 9; i++) { for (int i = 0; i < ingredients.size() && i < 9; i++) {
var ingredient = ingredients.get(i); var ingredient = ingredients.get(i);
if (!ingredient.isEmpty()) { if (!ingredient.isEmpty()) {
var items = ingredient.getItems(); templateItems.set(i, menu.pickAvailableTemplate(ingredient, stored, craftable, reserved));
if (items.length > 0) {
templateItems.set(i, items[0].copyWithCount(1));
}
} }
} }
@@ -145,42 +146,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,17 +182,19 @@ 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);
} }
}
poseStack.popPose(); poseStack.popPose();
} }
@@ -221,15 +210,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"));
} }
} }
} }
@@ -19,7 +19,6 @@ import appeng.crafting.execution.CraftingSubmitResult;
import net.minecraft.world.entity.player.Player; import net.minecraft.world.entity.player.Player;
import thaumcraft.common.research.TCResearchManager; import thaumcraft.common.research.TCResearchManager;
import thaumicenergistics.ThaumicEnergistics;
import thaumicenergistics.common.integration.tc.PatternResearchHelper; import thaumicenergistics.common.integration.tc.PatternResearchHelper;
/** /**
@@ -55,9 +54,6 @@ public abstract class MixinCraftingService {
continue; // 无研究要求 continue; // 无研究要求
} }
if (!TCResearchManager.has(player, research)) { if (!TCResearchManager.has(player, research)) {
ThaumicEnergistics.LOG.info(
"[AE2ResearchGate] 玩家 {} 未解锁研究 {},拒绝合成请求 (pattern={})",
player.getName().getString(), research, pattern.getDefinition());
// 通过自定义网络包发送研究键到客户端(AE2 errorDetail 不跨网络传输) // 通过自定义网络包发送研究键到客户端(AE2 errorDetail 不跨网络传输)
if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { if (player instanceof net.minecraft.server.level.ServerPlayer serverPlayer) {
net.neoforged.neoforge.network.PacketDistributor.sendToPlayer( net.neoforged.neoforge.network.PacketDistributor.sendToPlayer(
@@ -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,129 @@
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();
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
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);
}
}
/** 源质容器:仅要素安瓿与要素罐子(天域之华/水晶等按 Kind 排除)。 */
private static boolean isSourceContainer(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(stack);
}
/** 若手持物品是源质容器且非空,返回 true。 */
private static boolean hasContainerEssentia(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.hasEssentia(stack);
}
}
@@ -0,0 +1,128 @@
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.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import appeng.client.gui.me.common.RepoSlot;
import appeng.menu.me.common.MEStorageMenu;
import net.neoforged.neoforge.network.PacketDistributor;
import thaumicenergistics.common.network.EssentiaTerminalDepositC2SPacket;
import thaumicenergistics.common.network.EssentiaTerminalFillC2SPacket;
/**
* 让 WCWT(ME 综合工作终端)的屏幕支持源质右键交互。
* <p>
* WCWT 的 {@code WirelessComprehensiveWorkTerminalScreen} 覆写了 {@code mouseClicked},
* 因此注入到 {@code MEStorageScreen} 的 {@code MixinMEStorageScreen} 不会生效——
* 需直接对 WCWT 屏幕注入。逻辑与 {@code MixinMEStorageScreen} 完全一致:
* <ul>
* <li>Shift+右键玩家背包格(源质容器)→ 存入网络。</li>
* <li>右键网络区域手持满容器 → 存入网络。</li>
* <li>手持空容器右键源质条目 → 取出装罐。</li>
* </ul>
*/
@Mixin(targets = "com.lhy.wcwt.client.WirelessComprehensiveWorkTerminalScreen", remap = false)
public abstract class MixinWCWTScreen {
@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;
}
AbstractContainerScreen<?> self = (AbstractContainerScreen<?>) (Object) this;
MEStorageMenu menu = (MEStorageMenu) self.getMenu();
Slot hovered = self.getSlotUnderMouse();
boolean shiftDown = AbstractContainerScreen.hasShiftDown();
var carried = menu.getCarried();
// 源质物品来源:优先菜单 carried(拾取物品);为空时回退到玩家主手物品
ItemStack sourceStack = !carried.isEmpty() ? carried
: menu.getPlayerInventory().player.getMainHandItem();
boolean isContainer = isSourceContainer(sourceStack);
boolean containerHasEssentia = isContainer && hasContainerEssentia(sourceStack);
// Shift+右键玩家物品栏/背包格:格内是源质容器 → 存入源质(空容器留原格)
if (shiftDown && hovered != null && menu.isPlayerSideSlot(hovered)) {
var slotStack = hovered.getItem();
// 仅要素安瓿/要素罐子;天域之华等其余 IEssentiaContainerItem 一律放行
if (thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(slotStack)) {
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromSlot(
menu.containerId, hovered.getSlotIndex(), slotStack));
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(AE2 原版 Shift+右键快速移动)
return;
}
// 普通右键点击玩家物品栏/背包槽 → 放行(AE2 原版物品交互)
if (!shiftDown && hovered != null && menu.isPlayerSideSlot(hovered)) {
return;
}
// Shift+右键:手持源质容器(满或空)→ 拦截。满→存入源质;空→阻止被当作普通物品放入网络
if (shiftDown && isContainer) {
if (containerHasEssentia) {
clearSource(menu, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
menu.containerId, sourceStack));
}
cir.setReturnValue(true);
return;
}
// 普通右键网络区域:手持满容器 → 存入源质
if (containerHasEssentia) {
clearSource(menu, sourceStack, carried);
PacketDistributor.sendToServer(EssentiaTerminalDepositC2SPacket.fromClientStack(
menu.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(
menu.containerId, key.getId(), sourceStack));
cir.setReturnValue(true);
return;
}
}
cir.setReturnValue(true);
return;
}
// 非源质容器 → 放行(WCWT 原版右键)
}
/** 清空 source 所在位置(carried 或主手),防止被 AE2 后续当普通物品处理。 */
private static void clearSource(MEStorageMenu menu, ItemStack source, ItemStack carried) {
if (!carried.isEmpty() && carried == source) {
menu.setCarried(net.minecraft.world.item.ItemStack.EMPTY);
} else {
menu.getPlayerInventory().player.getInventory().setItem(
menu.getPlayerInventory().player.getInventory().selected,
net.minecraft.world.item.ItemStack.EMPTY);
}
}
/** 源质容器:仅要素安瓿与要素罐子(天域之华/水晶等按 Kind 排除)。 */
private static boolean isSourceContainer(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.isEssentiaContainer(stack);
}
/** 若手持物品是源质容器且非空,返回 true。 */
private static boolean hasContainerEssentia(ItemStack stack) {
return thaumicenergistics.common.integration.tc.ThEEssentiaContainers.hasEssentia(stack);
}
}
@@ -0,0 +1,52 @@
package thaumicenergistics.mixins.eap;
import net.minecraft.world.item.ItemStack;
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 appeng.api.stacks.GenericStack;
import mezz.jei.api.ingredients.ITypedIngredient;
import thaumcraft.api.aspects.Aspect;
import thaumicenergistics.common.integration.appeng.AEssentiaKey;
import thaumicenergistics.common.integration.tc.TCReflection;
import thaumicenergistics.integration.jei.AspectIngredient;
/**
* ExtendedAE-Plus 兼容(可选,eap 未安装时静默跳过):
* 其 {@code InputEvents.toGenericStack} 只认 ItemStack/FluidStack,
* 我们收藏的源质(AspectIngredient)会返回 null → 中键/Shift+左键 无效。
* 此处把源质转成 {@code GenericStack(AEssentiaKey)},让 eap 的中键拉起合成、拉取流程正常走通。
*/
@Mixin(targets = "com.extendedae_plus.client.InputEvents", remap = false)
public abstract class MixinEAPInputEvents {
@Inject(method = "toGenericStack(Ljava/lang/Object;)Lappeng/api/stacks/GenericStack;",
at = @At("HEAD"), cancellable = true, remap = false)
private static void thE$essentiaToGenericStack(Object typed, CallbackInfoReturnable<GenericStack> cir) {
if (!(typed instanceof ITypedIngredient<?> typedIngredient)) {
return;
}
Object value = typedIngredient.getIngredient();
AEssentiaKey key = null;
// 新收藏:AspectIngredient(ThEEssentiaGuiHandler 提供,有图标+中文名)
if (value instanceof AspectIngredient ai && ai.aspect() != null) {
Aspect aspect = ai.aspect();
var id = TCReflection.getAspectId(aspect);
if (id != null) {
key = AEssentiaKey.of(id);
}
} else if (value instanceof ItemStack itemStack) {
// 旧收藏/兜底:WrappedGenericStack 物品包裹的形式
GenericStack unwrapped = GenericStack.unwrapItemStack(itemStack);
if (unwrapped != null && unwrapped.what() instanceof AEssentiaKey wrappedKey) {
key = wrappedKey;
}
}
if (key != null) {
cir.setReturnValue(new GenericStack(key, 1));
}
}
}
+11
View File
@@ -0,0 +1,11 @@
modLoader="javafml"
loaderVersion="[4,)"
license="MIT"
[[mods]]
modId="thaumicenergistics"
version="2.0.1"
displayName="ThaumicEnergistics"
description="Bridges Thaumcraft Essentia with Applied Energistics 2 ME networks."
authors="Nividica, Community"
logoFile="assets/thaumicenergistics/meta/logo.png"
@@ -35,7 +35,7 @@ side="BOTH"
[[dependencies.thaumicenergistics]] [[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"
@@ -13,7 +13,12 @@
}, },
"STORAGE": { "STORAGE": {
"left": 134, "left": 134,
"bottom": 163 "bottom": 162
},
"TOOLBOX": {
"left": 175,
"bottom": 84,
"grid": "BREAK_AFTER_3COLS"
} }
}, },
"text": { "text": {
@@ -32,30 +37,59 @@
}, },
"position": { "position": {
"left": 8, "left": 8,
"bottom": 177 "bottom": 179
}
},
"player_inventory_title": {
"text": {
"translate": "container.inventory"
},
"position": {
"left": 8,
"bottom": 94
} }
} }
}, },
"terminalStyle": { "terminalStyle": {
"header": { "header": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png", "texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 0, 195, 17] "srcRect": [0, 0, 194, 17]
}, },
"firstRow": { "firstRow": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png", "texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 17, 195, 18] "srcRect": [0, 17, 194, 18]
}, },
"row": { "row": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png", "texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 35, 195, 18] "srcRect": [0, 35, 194, 18]
}, },
"lastRow": { "lastRow": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png", "texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 53, 195, 18] "srcRect": [0, 53, 194, 18]
}, },
"bottom": { "bottom": {
"texture": "thaumicenergistics:textures/gui/arcane_crafting.png", "texture": "thaumicenergistics:textures/gui/arcane_crafting.png",
"srcRect": [0, 71, 195, 180] "srcRect": [0, 71, 194, 180]
}
},
"widgets": {
"clearCraftingGrid": {
"left": 103,
"bottom": 160
},
"clearToPlayerInv": {
"left": 113,
"bottom": 160
},
"viewCells": {
"left": 193,
"top": 0
},
"toolbox": {
"left": 174,
"bottom": 90,
"width": 59,
"height": 66
} }
} }
} }
@@ -0,0 +1,12 @@
{
"variants": {
"facing=north,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 0 },
"facing=south,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 180 },
"facing=east,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 90 },
"facing=west,variant=false": { "model": "thaumicenergistics:block/alkusure86fumo", "y": 270 },
"facing=north,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 0 },
"facing=south,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 180 },
"facing=east,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 90 },
"facing=west,variant=true": { "model": "thaumicenergistics:block/a999csfumo", "y": 270 }
}
}
@@ -0,0 +1,22 @@
{
"multipart": [
{ "when": { "facing": "down" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "x": 90 } },
{ "when": { "facing": "up" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "x": -90 } },
{ "when": { "facing": "north" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 0 } },
{ "when": { "facing": "south" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 180 } },
{ "when": { "facing": "east" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 90 } },
{ "when": { "facing": "west" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_model", "y": 270 } },
{ "when": { "facing": "down", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "x": 90 } },
{ "when": { "facing": "up", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "x": -90 } },
{ "when": { "facing": "north", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 0 } },
{ "when": { "facing": "south", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 180 } },
{ "when": { "facing": "east", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 90 } },
{ "when": { "facing": "west", "connected": "false" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_off", "y": 270 } },
{ "when": { "facing": "down", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "x": 90 } },
{ "when": { "facing": "up", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "x": -90 } },
{ "when": { "facing": "north", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 0 } },
{ "when": { "facing": "south", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 180 } },
{ "when": { "facing": "east", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 90 } },
{ "when": { "facing": "west", "connected": "true" }, "apply": { "model": "thaumicenergistics:block/essentia_provider_connection_on", "y": 270 } }
]
}
@@ -0,0 +1,16 @@
{
"multipart": [
{ "when": { "facing": "north" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 180 } },
{ "when": { "facing": "south" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 0 } },
{ "when": { "facing": "east" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 270 } },
{ "when": { "facing": "west" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_off", "y": 90 } },
{ "when": { "facing": "north", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 180 } },
{ "when": { "facing": "south", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 0 } },
{ "when": { "facing": "east", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 270 } },
{ "when": { "facing": "west", "book": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_book", "y": 90 } },
{ "when": { "facing": "north", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 180 } },
{ "when": { "facing": "south", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 0 } },
{ "when": { "facing": "east", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 270 } },
{ "when": { "facing": "west", "book": "true", "network": "true" }, "apply": { "model": "thaumicenergistics:block/infusion_monitor_on", "y": 90 } }
]
}
@@ -49,16 +49,8 @@
"tc.research_name.DIGISENTIA": "Digitized Essentia", "tc.research_name.DIGISENTIA": "Digitized Essentia",
"tc.research_text.DIGISENTIA.stage.1": "Though essentia is a mystical substance, you have discovered it can be processed using AE's digital technology. The key lies in two cores: a Diffusion Core to decompose essentia into digital signals, and a Coalescence Core to reassemble digital signals back into essentia.", "tc.research_text.DIGISENTIA.stage.1": "Though essentia is a mystical substance, you have discovered it can be processed using AE's digital technology. The key lies in two cores: a Diffusion Core to decompose essentia into digital signals, and a Coalescence Core to reassemble digital signals back into essentia.",
"tc.research_text.DIGISENTIA.stage.2": "You have successfully crafted the Diffusion Core and Coalescence Core. They will serve as the foundation of your entire Thaumic Energistics system.", "tc.research_text.DIGISENTIA.stage.2": "You have successfully crafted the Diffusion Core and Coalescence Core. They will serve as the foundation of your entire Thaumic Energistics system.",
"tc.research_name.ESSENTIASTORAGE1K": "Essentia Storage: 1k",
"tc.research_text.ESSENTIASTORAGE1K": "Using the Coalescence Core and wisp essences, you can create ME storage cells capable of storing essentia. The 1k cell is the most basic model, holding a modest amount of a single essentia type.",
"tc.research_name.ESSENTIASTORAGE": "Essentia Storage", "tc.research_name.ESSENTIASTORAGE": "Essentia Storage",
"tc.research_text.ESSENTIASTORAGE": "Using Coalescence Cores and wisp essences, you can create ME storage cells capable of storing essentia. Available in 1k, 4k, 16k, and 64k variants.", "tc.research_text.ESSENTIASTORAGE": "Using Coalescence Cores and wisp essences, you can create ME storage cells capable of storing essentia. Available in 1k, 4k, 16k, and 64k variants.",
"tc.research_name.ESSENTIASTORAGE4K": "Essentia Storage: 4k",
"tc.research_text.ESSENTIASTORAGE4K": "Building upon the 1k cell, you can create a larger 4k essentia storage cell.",
"tc.research_name.ESSENTIASTORAGE16K": "Essentia Storage: 16k",
"tc.research_text.ESSENTIASTORAGE16K": "The 16k essentia storage cell can hold a considerable amount of essentia.",
"tc.research_name.ESSENTIASTORAGE64K": "Essentia Storage: 64k",
"tc.research_text.ESSENTIASTORAGE64K": "The 64k essentia storage cell represents the current pinnacle of this technology, capable of storing vast quantities of essentia.",
"tc.research_name.ESSENTIABUSES": "Essentia Buses", "tc.research_name.ESSENTIABUSES": "Essentia Buses",
"tc.research_text.ESSENTIABUSES.stage.1": "Using Diffusion and Coalescence Cores, you can create three types of essentia buses: the Import Bus extracts essentia from external containers into the ME network, the Export Bus outputs essentia from the network into external containers, and the Storage Bus maps external containers as ME network storage units.", "tc.research_text.ESSENTIABUSES.stage.1": "Using Diffusion and Coalescence Cores, you can create three types of essentia buses: the Import Bus extracts essentia from external containers into the ME network, the Export Bus outputs essentia from the network into external containers, and the Storage Bus maps external containers as ME network storage units.",
"tc.research_text.ESSENTIABUSES.stage.2": "Essentia buses are the bridge between the physical world and the digital essentia realm. Attach them to an ME cable facing an essentia container to activate them.", "tc.research_text.ESSENTIABUSES.stage.2": "Essentia buses are the bridge between the physical world and the digital essentia realm. Attach them to an ME cable facing an essentia container to activate them.",
@@ -110,6 +102,8 @@
"tc.research_name.PSEUDO_SCEPTRE": "Crafting Scepters", "tc.research_name.PSEUDO_SCEPTRE": "Crafting Scepters",
"tc.research_name.PSEUDO_COREGATHER": "Golem Core: Gather", "tc.research_name.PSEUDO_COREGATHER": "Golem Core: Gather",
"gui.thaumicenergistics.ArcaneCraftingTerminal": "Arcane Crafting Terminal",
"thaumicenergistics.jade.current_aspect": "Fuel: %s",
"thaumicenergistics.jade.suction_type": "Suction: %s", "thaumicenergistics.jade.suction_type": "Suction: %s",
"thaumicenergistics.jade.essentia_stored": "Essentia: %d / %d", "thaumicenergistics.jade.essentia_stored": "Essentia: %d / %d",
"thaumicenergistics.jade.burning": "Burning %d%%, %.1f AE/tick", "thaumicenergistics.jade.burning": "Burning %d%%, %.1f AE/tick",
@@ -119,5 +113,44 @@
"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.gui.encode": "Encode",
"thaumicenergistics.gui.knowledge_inscriber.no_core": "No Core",
"thaumicenergistics.gui.knowledge_inscriber.invalid": "Invalid",
"thaumicenergistics.gui.knowledge_inscriber.full": "Full",
"thaumicenergistics.gui.knowledge_inscriber.save": "Save",
"thaumicenergistics.gui.knowledge_inscriber.delete": "Delete",
"thaumicenergistics.jei.transfer.missing": "Missing: %d",
"thaumicenergistics.jei.transfer.craftable": "Craftable: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "Use Ctrl to craft missing items",
"block.thaumicenergistics.infusion_monitor": "Infusion Monitor",
"thaumicenergistics.jade.monitor.title": "Infusion Monitor",
"thaumicenergistics.jade.monitor.no_matrix": "No infusion altar detected",
"thaumicenergistics.jade.monitor.instability": "Instability: %s (base %s + altar %s)",
"thaumicenergistics.jade.monitor.crafting": "Infusion in progress",
"thaumicenergistics.jade.monitor.shortages": "Essentia shortage: %s",
"thaumicenergistics.jade.monitor.essence_ok": "Essentia sufficient",
"thaumicenergistics.jade.monitor.tier": "Risk: %s (%d/5)",
"thaumicenergistics.jade.tier.1": "Very Low",
"thaumicenergistics.jade.tier.2": "Low",
"thaumicenergistics.jade.tier.3": "Medium",
"thaumicenergistics.jade.tier.4": "High",
"thaumicenergistics.jade.tier.5": "Very High",
"thaumicenergistics.jade.provider.network": "Network: %s",
"thaumicenergistics.jade.provider.online": "Online",
"thaumicenergistics.jade.provider.offline": "Offline",
"thaumicenergistics.jade.provider.no_channel": "No Channel",
"thaumicenergistics.jade.assembler.vis_source": "Essentia Source: %s",
"thaumicenergistics.jade.assembler.vis_connected": "Connected",
"thaumicenergistics.jade.assembler.vis_disconnected": "Not Connected",
"block.thaumicenergistics.alkusure86fumo": "Alkusure86 Fumo",
"tc.research_text.INFUSIONMONITOR.stage.1": "To observe is to command. You place the Thaumonomicon beside the altar, an observer peering into each pulse of the infusion — the stirring of aura, the opening of rifts, the first whisper of flux. Bound to an ME network, the observer truly opens its eyes.",
"tc.research_text.INFUSIONMONITOR.stage.2": "The Infusion Monitor takes the Thaumonomicon as its key and the network as its sight. Place the tome and connect to the network, and it shall hover above the void, truthfully telling the risk of the ritual — and on right-click, it whispers its deeper warnings.",
"tc.research_name.INFUSIONMONITOR": "Infusion Monitor",
"tc.research_name.GOLEMWIFIBACKPACK": "Golem WiFi Backpack",
"block.thaumicenergistics.essentia_provider_connection": "Wireless Essentia Receiver",
"item.thaumicenergistics.wireless_connector": "Wireless Connector",
"tc.research_text.ESSENTIAPROVIDER.connection.stage.1": "The Wireless Essentia Receiver can be mounted on any surface. Use the Wireless Essentia Binding Tool to link it to an Essentia Provider, relaying essentia to alchemy towers and stills at a distance. Each provider serves up to eight receivers within thirty-two blocks; each additional receiver costs five extra AE/t. The receiver can also feed essentia back into the network, up to sixteen per transfer.",
"tc.research_text.ESSENTIAPROVIDER.tool.stage.1": "The Wireless Essentia Binding Tool creates and breaks wireless links: sneak-right-click an unlinked receiver to select it, then sneak-right-click an Essentia Provider to bind; sneak-right-click a linked receiver to disconnect. A normal right-click shows the connection coordinates of either device."
} }
@@ -58,14 +58,6 @@
"tc.research_name.ESSENTIASTORAGE": "源质存储", "tc.research_name.ESSENTIASTORAGE": "源质存储",
"tc.research_text.ESSENTIASTORAGE": "利用凝聚核心和天域之华制造ME源质存储元件,有1k、4k、16k、64k四种容量。", "tc.research_text.ESSENTIASTORAGE": "利用凝聚核心和天域之华制造ME源质存储元件,有1k、4k、16k、64k四种容量。",
"tc.research_name.ESSENTIASTORAGE1K": "源质存储:1k",
"tc.research_text.ESSENTIASTORAGE1K": "利用凝聚核心和天域之华,你可以制造出能够存储源质的ME存储元件。1k元件是最基础的型号,可以存储少量单一类型的源质。",
"tc.research_name.ESSENTIASTORAGE4K": "源质存储:4k",
"tc.research_text.ESSENTIASTORAGE4K": "在1k元件的基础上,你可以制造容量更大的4k源质存储元件。",
"tc.research_name.ESSENTIASTORAGE16K": "源质存储:16k",
"tc.research_text.ESSENTIASTORAGE16K": "16k源质存储元件能够存储相当可观的源质。",
"tc.research_name.ESSENTIASTORAGE64K": "源质存储:64k",
"tc.research_text.ESSENTIASTORAGE64K": "64k源质存储元件是当前技术的极限,能够存储海量源质。",
"tc.research_name.ESSENTIABUSES": "源质总线", "tc.research_name.ESSENTIABUSES": "源质总线",
"tc.research_text.ESSENTIABUSES.stage.1": "通过扩散核心和凝聚核心,你可以制造出三种源质总线:输入总线从外部容器抽取源质进入ME网络,输出总线将网络中的源质输出到外部容器,存储总线则将外部容器映射为ME网络的存储单元。", "tc.research_text.ESSENTIABUSES.stage.1": "通过扩散核心和凝聚核心,你可以制造出三种源质总线:输入总线从外部容器抽取源质进入ME网络,输出总线将网络中的源质输出到外部容器,存储总线则将外部容器映射为ME网络的存储单元。",
@@ -129,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": "火",
@@ -138,5 +130,40 @@
"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.gui.encode": "编码",
"thaumicenergistics.gui.knowledge_inscriber.no_core": "缺核",
"thaumicenergistics.gui.knowledge_inscriber.invalid": "无效",
"thaumicenergistics.gui.knowledge_inscriber.full": "已满",
"thaumicenergistics.gui.knowledge_inscriber.save": "保存",
"thaumicenergistics.gui.knowledge_inscriber.delete": "删除",
"thaumicenergistics.jei.transfer.missing": "缺少: %d",
"thaumicenergistics.jei.transfer.craftable": "可合成: %d",
"thaumicenergistics.jei.transfer.ctrl_click": "按住 Ctrl 合成缺失物品",
"block.thaumicenergistics.infusion_monitor": "韵魔观测器",
"thaumicenergistics.jade.monitor.title": "韵魔观测器",
"thaumicenergistics.jade.monitor.no_matrix": "未检测到注魔祭坛",
"thaumicenergistics.jade.monitor.instability": "不稳定度: %s (基础 %s + 祭坛 %s)",
"thaumicenergistics.jade.monitor.crafting": "注魔进行中",
"thaumicenergistics.jade.monitor.shortages": "源质缺口: %s",
"thaumicenergistics.jade.monitor.essence_ok": "源质充足",
"thaumicenergistics.jade.monitor.tier": "风险: %s(%d/5)",
"thaumicenergistics.jade.tier.1": "极低",
"thaumicenergistics.jade.tier.2": "低",
"thaumicenergistics.jade.tier.3": "中",
"thaumicenergistics.jade.tier.4": "高",
"thaumicenergistics.jade.tier.5": "极高",
"thaumicenergistics.jade.provider.network": "网络: %s",
"thaumicenergistics.jade.provider.online": "在线",
"thaumicenergistics.jade.provider.offline": "离线",
"thaumicenergistics.jade.provider.no_channel": "无频道",
"block.thaumicenergistics.alkusure86fumo": "Alkusure86 玩偶",
"tc.research_text.INFUSIONMONITOR.stage.1": "观测即是驾驭。你将魔导手册置于祭坛之侧,以侦测器窥探注魔的每一次脉动——灵光、裂隙、咒波的先兆,尽收眼底。当它与 ME 网络相连,观测者便真正睁开眼。",
"tc.research_text.INFUSIONMONITOR.stage.2": "韵魔观测器以魔导手册为钥、以网络为眼。放上手册并接入网络,它便会悬浮于虚空之中,如实告知你注魔之仪的风险——而按住右键,它将低语出更深层的告诫。",
"tc.research_name.INFUSIONMONITOR": "韵魔观测器",
"tc.research_name.GOLEMWIFIBACKPACK": "傀儡WiFi背包",
"block.thaumicenergistics.essentia_provider_connection": "源质无线接收器",
"item.thaumicenergistics.wireless_connector": "源质无线绑定工具",
"tc.research_text.ESSENTIAPROVIDER.connection.stage.1": "源质无线接收器可挂置于任意表面,以源质无线绑定工具与源质供应器建立无线连接,将源质远程输送至炼金塔、蒸馏器等设施。每台供应器最多服务八台接收器,距离不得超过三十二格;每多一台接收器,供应器功耗上升五点。接收器亦可向网络回输源质,每次上限十六点。",
"tc.research_text.ESSENTIAPROVIDER.tool.stage.1": "源质无线绑定工具用于建立与解除无线连接:按住 Shift 右键未连接的接收器将其选中,再 Shift 右键源质供应器完成绑定;Shift 右键已连接的接收器可断开连接;普通右键则可查看双方连接坐标。"
} }
@@ -0,0 +1,216 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"render_type": "cutout",
"textures": {
"1": "thaumicenergistics:block/alkusure86fumo",
"particle": "thaumicenergistics:block/alkusure86fumo"
},
"elements": [
{
"name": "Head",
"from": [4.35, 4.48613, 8.33113],
"to": [11.65, 11.78613, 15.63113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.21113, 11.90613]},
"faces": {
"north": {"uv": [2, 2, 4, 4], "texture": "#1"},
"east": {"uv": [0, 2, 2, 4], "texture": "#1"},
"south": {"uv": [6, 2, 8, 4], "texture": "#1"},
"west": {"uv": [4, 2, 6, 4], "texture": "#1"},
"up": {"uv": [4, 2, 2, 0], "texture": "#1"},
"down": {"uv": [4, 0, 6, 2], "texture": "#1"}
}
},
{
"name": "Hat Layer",
"from": [4.1, 4.48613, 7.98113],
"to": [11.8, 12.08613, 15.68113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.21113, 11.90613]},
"faces": {
"north": {"uv": [10, 2, 12, 4], "texture": "#1"},
"east": {"uv": [8, 2, 10, 4], "texture": "#1"},
"south": {"uv": [14, 2, 16, 4], "texture": "#1"},
"west": {"uv": [12, 2, 14, 4], "texture": "#1"},
"up": {"uv": [12, 2, 10, 0], "texture": "#1"},
"down": {"uv": [12, 0, 14, 2], "texture": "#1"}
}
},
{
"name": "Left Arm",
"from": [3.89, -0.57401, 9.01167],
"to": [5.39, 5.42599, 11.01167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.42599, 10.01167]},
"faces": {
"north": {"uv": [9, 13, 9.75, 16], "texture": "#1"},
"east": {"uv": [8, 13, 9, 16], "texture": "#1"},
"south": {"uv": [10.75, 13, 11.5, 16], "texture": "#1"},
"west": {"uv": [9.75, 13, 10.5, 16], "texture": "#1"},
"up": {"uv": [9, 12, 9.75, 13], "texture": "#1"},
"down": {"uv": [9.75, 12, 10.5, 13], "texture": "#1"}
}
},
{
"name": "Left Arm Layer",
"from": [3.8154, -0.67401, 8.91167],
"to": [5.5154, 5.52599, 11.11167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.42599, 10.01167]},
"faces": {
"north": {"uv": [13, 13, 13.75, 16], "texture": "#1"},
"east": {"uv": [12, 13, 13, 16], "texture": "#1"},
"south": {"uv": [14.5, 13, 15.25, 16], "texture": "#1"},
"west": {"uv": [13.75, 13, 14.5, 16], "texture": "#1"},
"up": {"uv": [13, 12, 13.75, 13], "texture": "#1"},
"down": {"uv": [13.75, 12, 14.5, 13], "texture": "#1"}
}
},
{
"name": "RightArm",
"from": [10.48, -0.65754, 8.979],
"to": [11.98, 5.34246, 10.979],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.13663, 2.34246, 9.979]},
"faces": {
"north": {"uv": [11, 5, 11.75, 8], "texture": "#1"},
"east": {"uv": [10, 5, 11, 8], "texture": "#1"},
"south": {"uv": [12.75, 5, 13.5, 8], "texture": "#1"},
"west": {"uv": [11.75, 5, 12.5, 8], "texture": "#1"},
"up": {"uv": [11, 4, 11.75, 5], "texture": "#1"},
"down": {"uv": [12.5, 4, 11.75, 5], "texture": "#1"}
}
},
{
"name": "Right Arm Layer",
"from": [10.36163, -0.75754, 8.879],
"to": [12.06163, 5.44246, 11.079],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.16163, 2.34246, 9.979]},
"faces": {
"north": {"uv": [11, 9, 11.75, 12], "texture": "#1"},
"east": {"uv": [10, 9, 11, 12], "texture": "#1"},
"south": {"uv": [12.75, 9, 13.5, 12], "texture": "#1"},
"west": {"uv": [11.75, 9, 12.5, 12], "texture": "#1"},
"up": {"uv": [11, 8, 11.75, 9], "texture": "#1"},
"down": {"uv": [12.5, 8, 11.75, 9], "texture": "#1"}
}
},
{
"name": "Left Leg",
"from": [7.77, -0.23, -0.23],
"to": [10.23, 2.23, 6.23],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 4, 3, 5], "texture": "#1"},
"east": {"uv": [0, 8, 1, 5], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 4, 2, 5], "texture": "#1"},
"west": {"uv": [2, 5, 3, 8], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 8, 1, 5], "texture": "#1"},
"down": {"uv": [3, 5, 4, 8], "texture": "#1"}
}
},
{
"name": "Left Leg Layer",
"from": [7.67, -0.33, -0.33],
"to": [10.33, 2.33, 6.33],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 8, 3, 9], "texture": "#1"},
"east": {"uv": [0, 12, 1, 9], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 8, 2, 9], "texture": "#1"},
"west": {"uv": [2, 9, 3, 12], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 12, 1, 9], "texture": "#1"},
"down": {"uv": [3, 9, 4, 12], "texture": "#1"}
}
},
{
"name": "Right Leg",
"from": [5.77, -0.23, -0.23],
"to": [8.23, 2.23, 6.23],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [6, 12, 7, 13], "texture": "#1"},
"east": {"uv": [4, 16, 5, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [5, 12, 6, 13], "texture": "#1"},
"west": {"uv": [6, 13, 7, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [6, 16, 5, 13], "texture": "#1"},
"down": {"uv": [7, 13, 8, 16], "texture": "#1"}
}
},
{
"name": "Right Leg Layer",
"from": [5.67, -0.33, -0.33],
"to": [8.33, 2.33, 6.33],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.1, 7]},
"faces": {
"north": {"uv": [2, 12, 3, 13], "texture": "#1"},
"east": {"uv": [0, 16, 1, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 12, 2, 13], "texture": "#1"},
"west": {"uv": [2, 13, 3, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 16, 1, 13], "texture": "#1"},
"down": {"uv": [3, 13, 4, 16], "texture": "#1"}
}
},
{
"name": "Body",
"from": [5.6, -1, 9.6],
"to": [10.4, 5, 12.4],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.4, 10.925]},
"faces": {
"north": {"uv": [5, 5, 7, 8], "texture": "#1"},
"east": {"uv": [4, 5, 5, 8], "texture": "#1"},
"south": {"uv": [8, 5, 10, 8], "texture": "#1"},
"west": {"uv": [7, 5, 8, 8], "texture": "#1"},
"up": {"uv": [5, 5, 7, 4], "texture": "#1"},
"down": {"uv": [7, 5, 9, 4], "texture": "#1"}
}
},
{
"name": "Body Layer",
"from": [5.5, -1.1, 9.5],
"to": [10.5, 5.1, 12.5],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.4, 10.925]},
"faces": {
"north": {"uv": [5, 9, 7, 12], "texture": "#1"},
"east": {"uv": [4, 9, 5, 12], "texture": "#1"},
"south": {"uv": [8, 9, 10, 12], "texture": "#1"},
"west": {"uv": [7, 9, 8, 12], "texture": "#1"},
"up": {"uv": [5, 9, 7, 8], "texture": "#1"},
"down": {"uv": [7, 9, 9, 8], "texture": "#1"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [60, 0, 0],
"translation": [0.25, 4.25, 1.25],
"scale": [0.7, 0.7, 0.7]
},
"thirdperson_lefthand": {
"rotation": [60, 0, 0],
"translation": [0.25, 4.25, 1.25],
"scale": [0.7, 0.7, 0.7]
},
"firstperson_righthand": {
"rotation": [0, 120, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_lefthand": {
"rotation": [0, 123, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"ground": {
"translation": [0, 2, 0],
"scale": [0.5, 0.5, 0.5]
},
"gui": {
"rotation": [15, 150, 0],
"translation": [-0.25, 0.25, 0],
"scale": [0.9, 0.9, 0.9]
},
"head": {
"translation": [12.75, 1.5, 0]
},
"fixed": {
"translation": [0, 0, -3.5]
}
}
}
@@ -0,0 +1,217 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"render_type": "cutout",
"textures": {
"1": "thaumicenergistics:block/alkusure86fumo",
"particle": "thaumicenergistics:block/alkusure86fumo"
},
"elements": [
{
"name": "Head",
"from": [4.35, 4.69613, 8.33113],
"to": [11.65, 11.99613, 15.63113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.42113, 11.90613]},
"faces": {
"north": {"uv": [2, 2, 4, 4], "texture": "#1"},
"east": {"uv": [0, 2, 2, 4], "texture": "#1"},
"south": {"uv": [6, 2, 8, 4], "texture": "#1"},
"west": {"uv": [4, 2, 6, 4], "texture": "#1"},
"up": {"uv": [4, 2, 2, 0], "texture": "#1"},
"down": {"uv": [4, 0, 6, 2], "texture": "#1"}
}
},
{
"name": "Hat Layer",
"from": [4.1, 4.69613, 7.98113],
"to": [11.8, 12.29613, 15.68113],
"rotation": {"angle": 22.5, "axis": "x", "origin": [7.975, 8.42113, 11.90613]},
"faces": {
"north": {"uv": [10, 2, 12, 4], "texture": "#1"},
"east": {"uv": [8, 2, 10, 4], "texture": "#1"},
"south": {"uv": [14, 2, 16, 4], "texture": "#1"},
"west": {"uv": [12, 2, 14, 4], "texture": "#1"},
"up": {"uv": [12, 2, 10, 0], "texture": "#1"},
"down": {"uv": [12, 0, 14, 2], "texture": "#1"}
}
},
{
"name": "Left Arm",
"from": [3.89, -0.36401, 9.01167],
"to": [5.39, 5.63599, 11.01167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.63599, 10.01167]},
"faces": {
"north": {"uv": [9, 13, 9.75, 16], "texture": "#1"},
"east": {"uv": [8, 13, 9, 16], "texture": "#1"},
"south": {"uv": [10.75, 13, 11.5, 16], "texture": "#1"},
"west": {"uv": [9.75, 13, 10.5, 16], "texture": "#1"},
"up": {"uv": [9, 12, 9.75, 13], "texture": "#1"},
"down": {"uv": [10.5, 12, 9.75, 13], "texture": "#1"}
}
},
{
"name": "Left Arm Layer",
"from": [3.8154, -0.46401, 8.91167],
"to": [5.5154, 5.73599, 11.11167],
"rotation": {"angle": -22.5, "axis": "z", "origin": [4.6527, 2.63599, 10.01167]},
"faces": {
"north": {"uv": [13, 13, 13.75, 16], "texture": "#1"},
"east": {"uv": [12, 13, 13, 16], "texture": "#1"},
"south": {"uv": [14.5, 13, 15.25, 16], "texture": "#1"},
"west": {"uv": [13.75, 13, 14.5, 16], "texture": "#1"},
"up": {"uv": [13, 12, 13.75, 13], "texture": "#1"},
"down": {"uv": [13.75, 12, 14.5, 13], "texture": "#1"}
}
},
{
"name": "RightArm",
"from": [10.48, -0.44754, 8.979],
"to": [11.98, 5.55246, 10.979],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.13663, 2.55246, 9.979]},
"faces": {
"north": {"uv": [11, 5, 11.75, 8], "texture": "#1"},
"east": {"uv": [10, 5, 11, 8], "texture": "#1"},
"south": {"uv": [12.75, 5, 13.5, 8], "texture": "#1"},
"west": {"uv": [11.75, 5, 12.5, 8], "texture": "#1"},
"up": {"uv": [11, 4, 11.75, 5], "texture": "#1"},
"down": {"uv": [12.5, 4, 11.75, 5], "texture": "#1"}
}
},
{
"name": "Right Arm Layer",
"from": [10.36163, -0.54754, 8.879],
"to": [12.06163, 5.65246, 11.079],
"rotation": {"angle": 22.5, "axis": "z", "origin": [11.16163, 2.55246, 9.979]},
"faces": {
"north": {"uv": [11, 9, 11.75, 12], "texture": "#1"},
"east": {"uv": [10, 9, 11, 12], "texture": "#1"},
"south": {"uv": [12.75, 9, 13.5, 12], "texture": "#1"},
"west": {"uv": [11.75, 9, 12.5, 12], "texture": "#1"},
"up": {"uv": [11, 8, 11.75, 9], "texture": "#1"},
"down": {"uv": [12.5, 8, 11.75, 9], "texture": "#1"}
}
},
{
"name": "Left Leg",
"from": [7.77, -0.02, -0.23],
"to": [10.23, 2.44, 6.23],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 4, 3, 5], "texture": "#1"},
"east": {"uv": [0, 8, 1, 5], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 4, 2, 5], "texture": "#1"},
"west": {"uv": [2, 5, 3, 8], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 8, 1, 5], "texture": "#1"},
"down": {"uv": [3, 5, 4, 8], "texture": "#1"}
}
},
{
"name": "Left Leg Layer",
"from": [7.67, -0.12, -0.33],
"to": [10.33, 2.54, 6.33],
"rotation": {"angle": -22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 8, 3, 9], "texture": "#1"},
"east": {"uv": [0, 12, 1, 9], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 8, 2, 9], "texture": "#1"},
"west": {"uv": [2, 9, 3, 12], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 12, 1, 9], "texture": "#1"},
"down": {"uv": [3, 9, 4, 12], "texture": "#1"}
}
},
{
"name": "Right Leg",
"from": [5.77, -0.02, -0.23],
"to": [8.23, 2.44, 6.23],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [6, 12, 7, 13], "texture": "#1"},
"east": {"uv": [4, 16, 5, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [5, 12, 6, 13], "texture": "#1"},
"west": {"uv": [6, 13, 7, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [6, 16, 5, 13], "texture": "#1"},
"down": {"uv": [7, 13, 8, 16], "texture": "#1"}
}
},
{
"name": "Right Leg Layer",
"from": [5.67, -0.12, -0.33],
"to": [8.33, 2.54, 6.33],
"rotation": {"angle": 22.5, "axis": "y", "origin": [8, 0.31, 7]},
"faces": {
"north": {"uv": [2, 12, 3, 13], "texture": "#1"},
"east": {"uv": [0, 16, 1, 13], "rotation": 90, "texture": "#1"},
"south": {"uv": [1, 12, 2, 13], "texture": "#1"},
"west": {"uv": [2, 13, 3, 16], "rotation": 90, "texture": "#1"},
"up": {"uv": [2, 16, 1, 13], "texture": "#1"},
"down": {"uv": [3, 13, 4, 16], "texture": "#1"}
}
},
{
"name": "Body",
"from": [5.6, -0.79, 9.6],
"to": [10.4, 5.21, 12.4],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.61, 10.925]},
"faces": {
"north": {"uv": [5, 5, 7, 8], "texture": "#1"},
"east": {"uv": [4, 5, 5, 8], "texture": "#1"},
"south": {"uv": [8, 5, 10, 8], "texture": "#1"},
"west": {"uv": [7, 5, 8, 8], "texture": "#1"},
"up": {"uv": [5, 5, 7, 4], "texture": "#1"},
"down": {"uv": [7, 5, 9, 4], "texture": "#1"}
}
},
{
"name": "Body Layer",
"from": [5.5, -0.89, 9.5],
"to": [10.5, 5.31, 12.5],
"rotation": {"angle": 45, "axis": "x", "origin": [7.975, 5.61, 10.925]},
"faces": {
"north": {"uv": [5, 9, 7, 12], "texture": "#1"},
"east": {"uv": [4, 9, 5, 12], "texture": "#1"},
"south": {"uv": [8, 9, 10, 12], "texture": "#1"},
"west": {"uv": [7, 9, 8, 12], "texture": "#1"},
"up": {"uv": [5, 9, 7, 8], "texture": "#1"},
"down": {"uv": [7, 9, 9, 8], "texture": "#1"}
}
}
],
"display": {
"thirdperson_righthand": {
"rotation": [37.5, 0, 0],
"translation": [0.25, 3, 3],
"scale": [0.5, 0.5, 0.5]
},
"thirdperson_lefthand": {
"rotation": [42.75, 0, 0],
"translation": [0.25, 2.75, 3.5],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_righthand": {
"rotation": [0, 120, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"firstperson_lefthand": {
"rotation": [0, 123, 0],
"translation": [0, 3.75, 0],
"scale": [0.5, 0.5, 0.5]
},
"ground": {
"translation": [0, 2, 0],
"scale": [0.5, 0.5, 0.5]
},
"gui": {
"rotation": [15, 150, 0],
"translation": [0, 1.75, 0],
"scale": [0.95, 0.95, 0.95]
},
"head": {
"translation": [12.75, 1.5, 0]
},
"fixed": {
"rotation": [-21.5, 0, 0],
"translation": [0, 0, -3.5]
}
}
}
@@ -0,0 +1,112 @@
{
"format_version": "1.9.0",
"credit": "Made with _leng",
"textures": {
"particle": "thaumicenergistics:block/essentia_provider_connection"
},
"elements": [
{
"from": [5, 5, 14],
"to": [11, 11, 16],
"faces": {
"north": {"uv": [0, 0, 6, 6], "texture": "#particle"},
"east": {"uv": [6, 6, 8, 0], "texture": "#particle"},
"south": {"uv": [0, 6, 6, 12], "texture": "#particle"},
"west": {"uv": [8, 0, 6, 6], "texture": "#particle"},
"up": {"uv": [6, 0, 8, 6], "rotation": 270, "texture": "#particle"},
"down": {"uv": [6, 0, 8, 6], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6.5, 6.5, 12],
"to": [9.5, 9.5, 12],
"faces": {
"north": {"uv": [6, 6, 9, 9], "texture": "#particle"}
}
},
{
"from": [6.25912, 7, 12.151],
"to": [7.25912, 9, 14.321],
"rotation": {"angle": -22.5, "axis": "y", "origin": [6.75912, 6.5, 12.681]},
"faces": {
"west": {"uv": [8, 0, 10, 2], "texture": "#particle"},
"up": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [9, 9, 8, 11], "texture": "#particle"}
}
},
{
"from": [8.74088, 7, 12.151],
"to": [9.74088, 9, 14.321],
"rotation": {"angle": 22.5, "axis": "y", "origin": [9.24088, 7.5, 12.681]},
"faces": {
"east": {"uv": [10, 0, 8, 2], "rotation": 270, "texture": "#particle"},
"up": {"uv": [8, 11, 9, 9], "texture": "#particle"},
"down": {"uv": [10, 7, 9, 9], "texture": "#particle"}
}
},
{
"from": [7, 5.83434, 12.0665],
"to": [9, 6.83434, 14.3065],
"rotation": {"angle": 22.5, "axis": "x", "origin": [8.4, 6.33434, 13.7065]},
"faces": {
"east": {"uv": [9, 11, 8, 9], "texture": "#particle"},
"west": {"uv": [10, 9, 9, 7], "texture": "#particle"},
"down": {"uv": [10, 0, 8, 2], "texture": "#particle"}
}
},
{
"from": [7, 9.16566, 12.0665],
"to": [9, 10.16566, 14.2365],
"rotation": {"angle": -22.5, "axis": "x", "origin": [8.4, 9.66566, 13.7065]},
"faces": {
"east": {"uv": [9, 7, 10, 9], "texture": "#particle"},
"west": {"uv": [8, 9, 9, 11], "texture": "#particle"},
"up": {"uv": [8, 0, 10, 2], "rotation": 90, "texture": "#particle"}
}
},
{
"from": [6, 6, 11.5],
"to": [7, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [6, 9, 11.5],
"to": [7, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 6, 11.5],
"to": [10, 7, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [9, 7, 12, 6], "rotation": 180, "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [12, 6, 9, 7], "rotation": 270, "texture": "#particle"}
}
},
{
"from": [9, 9, 11.5],
"to": [10, 10, 14],
"faces": {
"north": {"uv": [10, 0, 11, 1], "texture": "#particle"},
"east": {"uv": [9, 6, 12, 7], "texture": "#particle"},
"west": {"uv": [12, 7, 9, 6], "texture": "#particle"},
"up": {"uv": [12, 6, 9, 7], "rotation": 90, "texture": "#particle"},
"down": {"uv": [9, 6, 12, 7], "rotation": 90, "texture": "#particle"}
}
}
]
}

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