Compare commits
22
Commits
03cb045b3e
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b80922e1f1 | ||
|
|
2e6a8e9590 | ||
|
|
2ee393b4e6 | ||
|
|
0efd437209 | ||
|
|
ad0ead9f53 | ||
|
|
7a914571d8 | ||
|
|
14aaf0b1a9 | ||
|
|
177ef6c551 | ||
|
|
c83e34c1d6 | ||
|
|
0aa6db226e | ||
|
|
fc72ebca78 | ||
|
|
2e9f1fd4e3 | ||
|
|
835a37a0c7 | ||
|
|
da04b67ef0 | ||
|
|
13b981a916 | ||
|
|
286fc2d2c3 | ||
|
|
8673074e60 | ||
|
|
0e85bfc303 | ||
|
|
2d93c9d256 | ||
|
|
3573873518 | ||
|
|
c8adb56496 | ||
|
|
50969e9c26 |
+4983
-5
File diff suppressed because it is too large
Load Diff
@@ -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 移植团队完成,可提供更详细方案(含存储体系:内存缓存 + 可选本地二进制文件缓存,指纹校验跨启动复用)
|
||||||
@@ -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
@@ -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
|
||||||
+14
-1
@@ -1,11 +1,11 @@
|
|||||||
# Project
|
# Project
|
||||||
mod_version=2.1.0
|
|
||||||
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
|
||||||
@@ -16,3 +16,16 @@ ae2_version=19.2.17
|
|||||||
jei_version=19.39.0.368
|
jei_version=19.39.0.368
|
||||||
thaumcraft_version=0.2.2.34-port.1
|
thaumcraft_version=0.2.2.34-port.1
|
||||||
jade_version=15.9.1+neoforge
|
jade_version=15.9.1+neoforge
|
||||||
|
org.gradle.jvmargs=-Xmx2G
|
||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.caching=true
|
||||||
|
org.gradle.configuration-cache=true
|
||||||
|
minecraft_version_range=[1.21.1,1.22)
|
||||||
|
neoforge_version_range=[21,)
|
||||||
|
loader_version_range=[4,)
|
||||||
|
systemProp.http.proxyHost=127.0.0.1
|
||||||
|
systemProp.http.proxyPort=7890
|
||||||
|
systemProp.https.proxyHost=127.0.0.1
|
||||||
|
systemProp.https.proxyPort=7890
|
||||||
|
systemProp.http.nonProxyHosts=localhost|127.*
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
+2
-3
@@ -1,8 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.12-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -6,9 +6,4 @@ pluginManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
plugins {
|
rootProject.name = "thaumicenergistics-neo"
|
||||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
|
|
||||||
}
|
|
||||||
|
|
||||||
include("thaumicenergistics")
|
|
||||||
project(":thaumicenergistics").projectDir = file("modules/thaumicenergistics-neo")
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package thaumicenergistics.client;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.client.gui.GuiGraphics;
|
||||||
|
import net.minecraft.network.chat.Component;
|
||||||
|
|
||||||
|
import net.neoforged.api.distmarker.Dist;
|
||||||
|
import net.neoforged.bus.api.SubscribeEvent;
|
||||||
|
import net.neoforged.fml.common.EventBusSubscriber;
|
||||||
|
import net.neoforged.neoforge.client.event.ScreenEvent;
|
||||||
|
|
||||||
|
import appeng.api.networking.crafting.ICraftingSubmitResult;
|
||||||
|
import appeng.client.gui.me.crafting.CraftConfirmScreen;
|
||||||
|
import appeng.client.gui.me.crafting.CraftErrorScreen;
|
||||||
|
import appeng.menu.me.crafting.CraftConfirmMenu;
|
||||||
|
|
||||||
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端:研究未解锁拦截的提示渲染。
|
||||||
|
*
|
||||||
|
* 服务端拦截合成提交后,通过 ResearchGateErrorPacket 发送研究键;
|
||||||
|
* 本类在合成屏(确认屏/错误屏)之上绘制提示文字(最上层,不被 GUI 遮挡),
|
||||||
|
* 并暴露 hasResearchGateError() 供 MixinCraftConfirmScreen 判断是否拦截 CPU 忙错误屏。
|
||||||
|
*/
|
||||||
|
@EventBusSubscriber(modid = ThaumicEnergistics.MODID, value = Dist.CLIENT)
|
||||||
|
public final class ResearchGateOverlay {
|
||||||
|
|
||||||
|
private static String pendingResearch = null;
|
||||||
|
|
||||||
|
private ResearchGateOverlay() {}
|
||||||
|
|
||||||
|
/** 网络包处理:记录研究键(客户端线程)。 */
|
||||||
|
public static void setPendingResearch(String research) {
|
||||||
|
pendingResearch = research;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前菜单是否处于研究未解锁拦截状态。
|
||||||
|
* 依据 CraftConfirmMenu.submitError 判断:错误结果存在且 errorCode 非空,
|
||||||
|
* 说明本次提交被拒绝(研究拦截触发 CPU_BUSY)。
|
||||||
|
*/
|
||||||
|
public static boolean hasResearchGateError() {
|
||||||
|
return pendingResearch != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空研究拦截状态(玩家离开合成屏或成功重试时由外部调用)。 */
|
||||||
|
public static void clearPendingResearch() {
|
||||||
|
pendingResearch = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SubscribeEvent
|
||||||
|
public static void onScreenRenderPost(ScreenEvent.Render.Post event) {
|
||||||
|
// 玩家已离开合成屏(不是确认屏/错误屏)→ 清除拦截状态
|
||||||
|
if (!(event.getScreen() instanceof CraftConfirmScreen)
|
||||||
|
&& !(event.getScreen() instanceof CraftErrorScreen)) {
|
||||||
|
if (pendingResearch != null) {
|
||||||
|
clearPendingResearch();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 无拦截状态不渲染
|
||||||
|
if (pendingResearch == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GuiGraphics gui = event.getGuiGraphics();
|
||||||
|
Minecraft mc = Minecraft.getInstance();
|
||||||
|
int width = mc.getWindow().getGuiScaledWidth();
|
||||||
|
|
||||||
|
// 纯色无样式,保证与 MC 默认 GUI 字体一致
|
||||||
|
Component msg = Component.literal("当前研究节点未解锁:" + pendingResearch);
|
||||||
|
|
||||||
|
int textWidth = mc.font.width(msg);
|
||||||
|
int x = (width - textWidth) / 2;
|
||||||
|
int y = 30;
|
||||||
|
|
||||||
|
drawWithShadow(gui, mc, msg, x, y, 0xFF5555, 0x000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 以指定颜色绘制文字并叠 8 向同偏移描边,保证在全屏 GUI 之上可读。 */
|
||||||
|
private static void drawWithShadow(GuiGraphics gui, Minecraft mc, Component msg, int x, int y,
|
||||||
|
int textColor, int outlineColor) {
|
||||||
|
for (int dx = -1; dx <= 1; dx++) {
|
||||||
|
for (int dy = -1; dy <= 1; dy++) {
|
||||||
|
if (dx == 0 && dy == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
gui.drawString(mc.font, msg, x + dx, y + dy, outlineColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gui.drawString(mc.font, msg, x, y, textColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
+258
-41
@@ -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 实现细节影响。
|
||||||
|
|||||||
+28
-13
@@ -4,8 +4,10 @@ import appeng.api.crafting.PatternDetailsHelper;
|
|||||||
import appeng.api.stacks.AEItemKey;
|
import appeng.api.stacks.AEItemKey;
|
||||||
import appeng.api.stacks.GenericStack;
|
import appeng.api.stacks.GenericStack;
|
||||||
import appeng.core.definitions.AEItems;
|
import appeng.core.definitions.AEItems;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
import net.neoforged.neoforge.network.PacketDistributor;
|
import net.neoforged.neoforge.network.PacketDistributor;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
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.entity.player.Player;
|
import net.minecraft.world.entity.player.Player;
|
||||||
@@ -13,13 +15,15 @@ import net.minecraft.world.inventory.AbstractContainerMenu;
|
|||||||
import net.minecraft.world.inventory.ClickType;
|
import net.minecraft.world.inventory.ClickType;
|
||||||
import net.minecraft.world.inventory.Slot;
|
import net.minecraft.world.inventory.Slot;
|
||||||
import net.minecraft.world.item.ItemStack;
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.component.CustomData;
|
||||||
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 thaumcraft.api.ThaumcraftApiHelper;
|
import thaumcraft.api.ThaumcraftApiHelper;
|
||||||
import thaumcraft.api.aspects.Aspect;
|
import thaumcraft.api.aspects.Aspect;
|
||||||
import thaumcraft.api.aspects.AspectList;
|
import thaumcraft.api.aspects.AspectList;
|
||||||
|
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;
|
||||||
@@ -31,6 +35,10 @@ import java.util.List;
|
|||||||
|
|
||||||
public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
|
public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
|
||||||
|
|
||||||
|
/** 蒸馏编码统一要求的研究节点(要素蒸馏)。 */
|
||||||
|
private static final String RESEARCH_DISTILESSENTIA = "DISTILESSENTIA";
|
||||||
|
private static final String NBTKEY_RESEARCH = "research";
|
||||||
|
|
||||||
private static final int PLAYER_INV_Y = 150;
|
private static final int PLAYER_INV_Y = 150;
|
||||||
private static final int HOTBAR_Y = 208;
|
private static final int HOTBAR_Y = 208;
|
||||||
private static final int PLAYER_INV_X = 8;
|
private static final int PLAYER_INV_X = 8;
|
||||||
@@ -90,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 拖拽设置模板)
|
||||||
@@ -235,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) {
|
||||||
@@ -249,6 +250,15 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
|
|||||||
|
|
||||||
public void onEncodePattern() {
|
public void onEncodePattern() {
|
||||||
if (encoder == null) return;
|
if (encoder == null) return;
|
||||||
|
|
||||||
|
// 研究限制:必须解锁"要素蒸馏"才能配置物品→源质蒸馏样板
|
||||||
|
if (!TCResearchManager.has(playerInv.player, RESEARCH_DISTILESSENTIA)) {
|
||||||
|
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);
|
||||||
@@ -288,6 +298,11 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu {
|
|||||||
List.of(output)
|
List.of(output)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 在样板上标记研究键(供使用端按玩家校验)
|
||||||
|
CompoundTag researchTag = new CompoundTag();
|
||||||
|
researchTag.putString(NBTKEY_RESEARCH, RESEARCH_DISTILESSENTIA);
|
||||||
|
encoded.set(DataComponents.CUSTOM_DATA, CustomData.of(researchTag));
|
||||||
|
|
||||||
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, encoded);
|
encoder.inventory.setStackInSlot(TileDistillationPatternEncoder.SLOT_ENCODED_PATTERN, encoded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import thaumcraft.api.aspects.AspectList;
|
|||||||
import thaumcraft.api.crafting.IArcaneRecipe;
|
import thaumcraft.api.crafting.IArcaneRecipe;
|
||||||
import thaumcraft.api.crafting.ShapedArcaneRecipe;
|
import thaumcraft.api.crafting.ShapedArcaneRecipe;
|
||||||
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
|
import thaumcraft.api.crafting.ShapelessArcaneRecipe;
|
||||||
|
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;
|
||||||
@@ -31,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 {
|
||||||
|
|
||||||
@@ -120,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);
|
||||||
@@ -136,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() {
|
||||||
@@ -184,6 +182,8 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
syncKCoreToTile();
|
syncKCoreToTile();
|
||||||
|
// 放/取核心后同步按钮状态(可保存/可删除/无效)到客户端
|
||||||
|
sendSaveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearPatternSlots() {
|
private void clearPatternSlots() {
|
||||||
@@ -220,6 +220,13 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
|
|||||||
return CoreSaveState.Disabled_InvalidRecipe;
|
return CoreSaveState.Disabled_InvalidRecipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 研究限制:配方有研究要求且玩家未解锁 → 禁止保存
|
||||||
|
String research = activeRecipe.getResearch();
|
||||||
|
if (research != null && !research.isEmpty()
|
||||||
|
&& !TCResearchManager.has(playerInv.player, research)) {
|
||||||
|
return CoreSaveState.Disabled_InvalidRecipe;
|
||||||
|
}
|
||||||
|
|
||||||
boolean isNew = !kCoreHandler.hasPatternFor(result);
|
boolean isNew = !kCoreHandler.hasPatternFor(result);
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
return kCoreHandler.hasRoomToStorePattern()
|
return kCoreHandler.hasRoomToStorePattern()
|
||||||
@@ -364,6 +371,13 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
|
|||||||
private void savePattern() {
|
private void savePattern() {
|
||||||
if (activeRecipe == null) return;
|
if (activeRecipe == null) return;
|
||||||
|
|
||||||
|
// 研究限制:配方有研究要求时,操作者必须已解锁对应研究才能编码
|
||||||
|
String research = activeRecipe.getResearch();
|
||||||
|
if (research != null && !research.isEmpty()
|
||||||
|
&& !TCResearchManager.has(playerInv.player, research)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ItemStack[] inputs = new ItemStack[CRAFTING_GRID_SIZE];
|
ItemStack[] inputs = new ItemStack[CRAFTING_GRID_SIZE];
|
||||||
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
|
for (int i = 0; i < CRAFTING_GRID_SIZE; i++) {
|
||||||
ItemStack stack = craftingInventory.getStackInSlot(i);
|
ItemStack stack = craftingInventory.getStackInSlot(i);
|
||||||
@@ -376,7 +390,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu {
|
|||||||
}
|
}
|
||||||
ItemStack result = resultInventory.getStackInSlot(0).copy();
|
ItemStack result = resultInventory.getStackInSlot(0).copy();
|
||||||
|
|
||||||
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs);
|
ArcaneCraftingPattern pattern = new ArcaneCraftingPattern(aspects, result, inputs, research);
|
||||||
kCoreHandler.addPattern(pattern);
|
kCoreHandler.addPattern(pattern);
|
||||||
updatePatternSlots();
|
updatePatternSlots();
|
||||||
sendSaveState();
|
sendSaveState();
|
||||||
|
|||||||
@@ -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_<tag>),与 TC JarBlock.filledPhialStack 一致。 */
|
||||||
|
private static ItemStack filledPhialStack(String aspectTag) {
|
||||||
|
ResourceLocation id = ResourceLocation.fromNamespaceAndPath(
|
||||||
|
"thaumcraft", "phial_of_essentia_" + aspectTag);
|
||||||
|
net.minecraft.world.item.Item item = BuiltInRegistries.ITEM.getOptional(id)
|
||||||
|
.orElse(BuiltInRegistries.ITEM.get(
|
||||||
|
ResourceLocation.fromNamespaceAndPath("thaumcraft", "glass_phial")));
|
||||||
|
return new ItemStack(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package thaumicenergistics.common.container;
|
||||||
|
|
||||||
|
import net.minecraft.world.entity.player.Inventory;
|
||||||
|
import net.minecraft.world.entity.player.Player;
|
||||||
|
import net.minecraft.world.inventory.MenuType;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
import appeng.api.storage.ITerminalHost;
|
||||||
|
import appeng.menu.me.common.MEStorageMenu;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 源质终端的菜单(有线端与无线端共用)。
|
||||||
|
* <p>
|
||||||
|
* 继承 AE2 原版 {@link MEStorageMenu} 的全部普通物品行为(左键/Shift+点击/拖拽均按普通物品处理)。
|
||||||
|
* 源质的存入与取出统一由右键交互承担:
|
||||||
|
* <ul>
|
||||||
|
* <li>手持装有源质的罐子/安瓿 + 右键任意处 → 源质存入网络({@link EssentiaFillHelper#extractContainerContent})。</li>
|
||||||
|
* <li>手持空罐/空安瓿 + 右键源质条目 → 从网络取出装罐({@link #fillContainerFromGrid})。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public class EssentiaTerminalMenu extends MEStorageMenu {
|
||||||
|
|
||||||
|
public EssentiaTerminalMenu(MenuType<?> menuType, int id, Inventory ip, ITerminalHost host) {
|
||||||
|
super(menuType, id, ip, host);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键终端源质条目装罐:从网络提取源质,装入手持的源质罐子或玻璃安瓿。
|
||||||
|
* 具体逻辑见 {@link EssentiaFillHelper#fillContainerFromGrid}。
|
||||||
|
*
|
||||||
|
* @param aspectId 源质 aspect 的 ResourceLocation
|
||||||
|
* @return true = 处理成功(装罐/装安瓿),false = 无法处理
|
||||||
|
*/
|
||||||
|
public boolean fillContainerFromGrid(ResourceLocation aspectId) {
|
||||||
|
if (isClientSide()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var player = getPlayerInventory().player;
|
||||||
|
var carried = getCarried();
|
||||||
|
return EssentiaFillHelper.fillContainerFromGrid(
|
||||||
|
this.storage, this.energySource, getActionSource(), player, carried, aspectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 AppEngCraftingSlot) {
|
anchorY = slot.y;
|
||||||
cachedAnchorY = 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;
|
||||||
|
|||||||
+27
-4
@@ -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); }
|
|
||||||
}
|
|
||||||
@@ -72,10 +72,6 @@ public class GuiEssentiaLevelEmitter extends UpgradeableScreen<ContainerEssentia
|
|||||||
guiGraphics.drawString(this.font, aspect.getName(),
|
guiGraphics.drawString(this.font, aspect.getName(),
|
||||||
ASPECT_SLOT_X + 20, ASPECT_SLOT_Y + 4, 0xFFFFFF, false);
|
ASPECT_SLOT_X + 20, ASPECT_SLOT_Y + 4, 0xFFFFFF, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
guiGraphics.drawString(this.font,
|
|
||||||
"Current: " + this.menu.currentLevel,
|
|
||||||
8, 70, 0xAAAAAA, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
+185
-49
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,22 +5,22 @@ import thaumicenergistics.ThaumicEnergistics;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 傀儡无线背包皮肤枚举。
|
* 傀儡无线背包皮肤枚举。
|
||||||
* 每种皮肤对应一个纹理文件:{@code textures/model/golemBackpack/{texId}.png}。
|
* 每种皮肤对应一个纹理文件:{@code textures/model/golembackpack/{texId}.png}。
|
||||||
* 皮肤通过 AE2 Facade 物品右键傀儡来更换。
|
* 皮肤通过 AE2 Facade 物品右键傀儡来更换。
|
||||||
* 对应 1.7.10 的 {@code ItemGolemWirelessBackpack.BackpackSkins}。
|
* 对应 1.7.10 的 {@code ItemGolemWirelessBackpack.BackpackSkins}。
|
||||||
*/
|
*/
|
||||||
public enum BackpackSkins {
|
public enum BackpackSkins {
|
||||||
|
|
||||||
Thaumium("Thaum"),
|
Thaumium("thaum"),
|
||||||
Stone("Stone"),
|
Stone("stone"),
|
||||||
Straw("Straw"),
|
Straw("straw"),
|
||||||
Wood("Wood"),
|
Wood("wood"),
|
||||||
Flesh("Flesh"),
|
Flesh("flesh"),
|
||||||
Clay("Clay"),
|
Clay("clay"),
|
||||||
Iron("Iron"),
|
Iron("iron"),
|
||||||
Tallow("Tallow"),
|
Tallow("tallow"),
|
||||||
Gold("Gold"),
|
Gold("gold"),
|
||||||
Diamond("Diamond");
|
Diamond("diamond");
|
||||||
|
|
||||||
public static final BackpackSkins[] VALUES = values();
|
public static final BackpackSkins[] VALUES = values();
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ public enum BackpackSkins {
|
|||||||
public ResourceLocation getTextureLocation() {
|
public ResourceLocation getTextureLocation() {
|
||||||
if (this.texture == null) {
|
if (this.texture == null) {
|
||||||
this.texture = ResourceLocation.fromNamespaceAndPath(
|
this.texture = ResourceLocation.fromNamespaceAndPath(
|
||||||
ThaumicEnergistics.MODID, "textures/model/golemBackpack/" + this.texId + ".png");
|
ThaumicEnergistics.MODID, "textures/model/golembackpack/" + this.texId + ".png");
|
||||||
}
|
}
|
||||||
return this.texture;
|
return this.texture;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -383,11 +375,11 @@ public final class RecipeRegistration {
|
|||||||
return new ItemStack(s.get());
|
return new ItemStack(s.get());
|
||||||
}
|
}
|
||||||
private static ItemStack tc(String name) {
|
private static ItemStack tc(String name) {
|
||||||
return new ItemStack(BuiltInRegistries.ITEM.get(
|
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
|
||||||
ResourceLocation.fromNamespaceAndPath("thaumcraft", name)));
|
ResourceLocation.fromNamespaceAndPath("thaumcraft", name)));
|
||||||
}
|
}
|
||||||
private static ItemStack ae(String name) {
|
private static ItemStack ae(String name) {
|
||||||
return new ItemStack(BuiltInRegistries.ITEM.get(
|
return new ItemStack(net.minecraft.core.registries.BuiltInRegistries.ITEM.get(
|
||||||
ResourceLocation.fromNamespaceAndPath("ae2", name)));
|
ResourceLocation.fromNamespaceAndPath("ae2", name)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+139
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ public class ArcaneCraftingPattern {
|
|||||||
private static final String NBTKEY_INGREDIENT_NUM = "input#";
|
private static final String NBTKEY_INGREDIENT_NUM = "input#";
|
||||||
private static final String NBTKEY_RESULT = "output";
|
private static final String NBTKEY_RESULT = "output";
|
||||||
private static final String NBTKEY_ASPECTS = "aspects";
|
private static final String NBTKEY_ASPECTS = "aspects";
|
||||||
|
private static final String NBTKEY_RESEARCH = "research";
|
||||||
private static final int GRID_SIZE = 9;
|
private static final int GRID_SIZE = 9;
|
||||||
|
|
||||||
/** 全局 RegistryAccess,用于 NBT 读写 */
|
/** 全局 RegistryAccess,用于 NBT 读写 */
|
||||||
@@ -27,10 +28,17 @@ public class ArcaneCraftingPattern {
|
|||||||
protected ItemStack[] ingredients = new ItemStack[GRID_SIZE];
|
protected ItemStack[] ingredients = new ItemStack[GRID_SIZE];
|
||||||
protected ItemStack result = ItemStack.EMPTY;
|
protected ItemStack result = ItemStack.EMPTY;
|
||||||
protected boolean isValid;
|
protected boolean isValid;
|
||||||
|
/** 对应 TC 奥术配方的研究键(用于使用端按玩家校验解锁)。可为空(老数据/无研究配方)。 */
|
||||||
|
protected String research;
|
||||||
|
|
||||||
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients) {
|
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients) {
|
||||||
|
this(aspects, result, ingredients, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArcaneCraftingPattern(AspectList aspects, ItemStack result, ItemStack[] ingredients, String research) {
|
||||||
this.aspects = aspects.copy();
|
this.aspects = aspects.copy();
|
||||||
this.result = result.copy();
|
this.result = result.copy();
|
||||||
|
this.research = research;
|
||||||
|
|
||||||
boolean hasValidInput = false;
|
boolean hasValidInput = false;
|
||||||
for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) {
|
for (int i = 0; i < GRID_SIZE && i < ingredients.length; i++) {
|
||||||
@@ -72,6 +80,12 @@ public class ArcaneCraftingPattern {
|
|||||||
return aspects.getAmount(aspect);
|
return aspects.getAmount(aspect);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 对应 TC 奥术配方的研究键;无研究要求/老数据时可能为 null。 */
|
||||||
|
@org.jetbrains.annotations.Nullable
|
||||||
|
public String getResearch() {
|
||||||
|
return research;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static boolean canSubstituteFor(Object target, ItemStack input) {
|
public static boolean canSubstituteFor(Object target, ItemStack input) {
|
||||||
if (target instanceof ItemStack targetStack) {
|
if (target instanceof ItemStack targetStack) {
|
||||||
@@ -91,6 +105,10 @@ public class ArcaneCraftingPattern {
|
|||||||
this.ingredients = new ItemStack[GRID_SIZE];
|
this.ingredients = new ItemStack[GRID_SIZE];
|
||||||
this.isValid = true;
|
this.isValid = true;
|
||||||
|
|
||||||
|
if (data.contains(NBTKEY_RESEARCH)) {
|
||||||
|
this.research = data.getString(NBTKEY_RESEARCH);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.contains(NBTKEY_ASPECTS)) {
|
if (data.contains(NBTKEY_ASPECTS)) {
|
||||||
this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS));
|
this.aspects.readFromNBT(data.getCompound(NBTKEY_ASPECTS));
|
||||||
}
|
}
|
||||||
@@ -133,6 +151,10 @@ public class ArcaneCraftingPattern {
|
|||||||
this.aspects.writeToNBT(aspectTag);
|
this.aspects.writeToNBT(aspectTag);
|
||||||
data.put(NBTKEY_ASPECTS, aspectTag);
|
data.put(NBTKEY_ASPECTS, aspectTag);
|
||||||
|
|
||||||
|
if (this.research != null && !this.research.isEmpty()) {
|
||||||
|
data.putString(NBTKEY_RESEARCH, this.research);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < GRID_SIZE; i++) {
|
for (int i = 0; i < GRID_SIZE; i++) {
|
||||||
if (this.ingredients[i] == null || this.ingredients[i].isEmpty()) continue;
|
if (this.ingredients[i] == null || this.ingredients[i].isEmpty()) continue;
|
||||||
// 1.21.1: ItemStack.save(Provider) 返回新的 Tag,不会写入传入的 CompoundTag。
|
// 1.21.1: ItemStack.save(Provider) 返回新的 Tag,不会写入传入的 CompoundTag。
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package thaumicenergistics.common.integration.tc;
|
||||||
|
|
||||||
|
import appeng.api.crafting.IPatternDetails;
|
||||||
|
import appeng.api.stacks.AEItemKey;
|
||||||
|
import net.minecraft.core.component.DataComponents;
|
||||||
|
import net.minecraft.nbt.CompoundTag;
|
||||||
|
import net.minecraft.world.item.ItemStack;
|
||||||
|
import net.minecraft.world.item.component.CustomData;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
import thaumicenergistics.common.tiles.TileArcaneAssembler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 AE2 {@link IPatternDetails} 中提取 TC 研究键的辅助类。
|
||||||
|
*
|
||||||
|
* 两种样板携带研究键的方式:
|
||||||
|
* - 奥术装配器样板 {@link TileArcaneAssembler.ArcaneAssemblerPattern}:直接持 research 字段
|
||||||
|
* - 蒸馏处理样板(AE2 标准 processing pattern):通过样板上 DataComponents.CUSTOM_DATA 的 research 字段标记
|
||||||
|
*/
|
||||||
|
public final class PatternResearchHelper {
|
||||||
|
|
||||||
|
public static final String NBTKEY_RESEARCH = "research";
|
||||||
|
|
||||||
|
private PatternResearchHelper() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取样板对应的 TC 研究键;无研究要求或未知样板时返回 null(表示不限制)。
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public static String getResearch(IPatternDetails pattern) {
|
||||||
|
if (pattern instanceof TileArcaneAssembler.ArcaneAssemblerPattern arcanePattern) {
|
||||||
|
return arcanePattern.getResearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 蒸馏处理样板:从样板上 CUSTOM_DATA 读 research 标记
|
||||||
|
AEItemKey definition = pattern.getDefinition();
|
||||||
|
if (definition != null) {
|
||||||
|
ItemStack patternStack = definition.toStack();
|
||||||
|
CustomData customData = patternStack.get(DataComponents.CUSTOM_DATA);
|
||||||
|
if (customData != null) {
|
||||||
|
CompoundTag tag = customData.copyTag();
|
||||||
|
if (tag.contains(NBTKEY_RESEARCH)) {
|
||||||
|
String research = tag.getString(NBTKEY_RESEARCH);
|
||||||
|
return research.isEmpty() ? null : research;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 中创建一个占位节点,
|
||||||
@@ -87,18 +86,13 @@ public class PseudoResearchItem extends ResearchItem {
|
|||||||
} else {
|
} else {
|
||||||
// 最终备选:ThE 研究 tab 图标
|
// 最终备选:ThE 研究 tab 图标
|
||||||
pseudo = new PseudoResearchItem(pseudoKey, category, column, row,
|
pseudo = new PseudoResearchItem(pseudoKey, category, column, row,
|
||||||
ResourceLocation.fromNamespaceAndPath(ThaumicEnergistics.MODID, "textures/research/tab_icon.png"),
|
ResourceLocation.fromNamespaceAndPath(thaumicenergistics.ThaumicEnergistics.MODID, "textures/research/tab_icon.png"),
|
||||||
realKey, realCategory);
|
realKey, realCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存真实研究引用:内容(名称/文字/页面)通过方法委托到真实研究(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;
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
|
|||||||
private final int totalTypes;
|
private final int totalTypes;
|
||||||
private final double idleDrain;
|
private final double idleDrain;
|
||||||
|
|
||||||
public ItemEssentiaCell(Properties props, String tier, int kilobytes, int bytesPerType, int totalTypes, double idleDrain) {
|
public ItemEssentiaCell(Item.Properties props, String tier, int kilobytes, int bytesPerType, int totalTypes, double idleDrain) {
|
||||||
super(props.stacksTo(1));
|
super(props.stacksTo(1));
|
||||||
this.tier = tier;
|
this.tier = tier;
|
||||||
this.totalBytes = kilobytes * 1024;
|
this.totalBytes = kilobytes * 1024;
|
||||||
@@ -77,7 +82,7 @@ public class ItemEssentiaCell extends Item implements IBasicCellItem {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isBlackListed(ItemStack cellItem, appeng.api.stacks.AEKey requestedAddition) {
|
public boolean isBlackListed(ItemStack cellItem, appeng.api.stacks.AEKey requestedAddition) {
|
||||||
if (requestedAddition instanceof AEssentiaKey) {
|
if (requestedAddition instanceof thaumicenergistics.common.integration.appeng.AEssentiaKey) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -207,20 +212,52 @@ 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(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);
|
||||||
}
|
}
|
||||||
public static ItemEssentiaCell create4k(Properties props) {
|
public static ItemEssentiaCell create4k(Item.Properties props) {
|
||||||
return new ItemEssentiaCell(props, "4k", 4, 8, ThEConfig.CELL_MAX_TYPES, 1.0);
|
return new ItemEssentiaCell(props, "4k", 4, 8, ThEConfig.CELL_MAX_TYPES, 1.0);
|
||||||
}
|
}
|
||||||
public static ItemEssentiaCell create16k(Properties props) {
|
public static ItemEssentiaCell create16k(Item.Properties props) {
|
||||||
return new ItemEssentiaCell(props, "16k", 16, 8, ThEConfig.CELL_MAX_TYPES, 1.5);
|
return new ItemEssentiaCell(props, "16k", 16, 8, ThEConfig.CELL_MAX_TYPES, 1.5);
|
||||||
}
|
}
|
||||||
public static ItemEssentiaCell create64k(Properties props) {
|
public static ItemEssentiaCell create64k(Item.Properties props) {
|
||||||
return new ItemEssentiaCell(props, "64k", 64, 8, ThEConfig.CELL_MAX_TYPES, 2.0);
|
return new ItemEssentiaCell(props, "64k", 64, 8, ThEConfig.CELL_MAX_TYPES, 2.0);
|
||||||
}
|
}
|
||||||
public static ItemEssentiaCell createCreative(Properties props) {
|
public static ItemEssentiaCell createCreative(Item.Properties props) {
|
||||||
return new ItemEssentiaCell(props, "creative", Integer.MAX_VALUE / 1024, 8, 63, 0.0);
|
return new ItemEssentiaCell(props, "creative", Integer.MAX_VALUE / 1024, 8, 63, 0.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ public class ItemGolemWirelessBackpack extends Item {
|
|||||||
public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler();
|
public static final IGridLinkableHandler LINKABLE_HANDLER = new LinkableHandler();
|
||||||
|
|
||||||
public ItemGolemWirelessBackpack() {
|
public ItemGolemWirelessBackpack() {
|
||||||
super(new Properties().stacksTo(1).durability(0));
|
super(new Item.Properties().stacksTo(1).durability(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
public class PartItemEssentiaExportBus extends Item implements IPartItem<EssentiaExportBusPart> {
|
public class PartItemEssentiaExportBus extends Item implements IPartItem<EssentiaExportBusPart> {
|
||||||
|
|
||||||
public static PartItemEssentiaExportBus create() {
|
public static PartItemEssentiaExportBus create() {
|
||||||
return new PartItemEssentiaExportBus(new Properties().stacksTo(64));
|
return new PartItemEssentiaExportBus(new Item.Properties().stacksTo(64));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartItemEssentiaExportBus(Properties properties) {
|
public PartItemEssentiaExportBus(Properties properties) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
public class PartItemEssentiaImportBus extends Item implements IPartItem<EssentiaImportBusPart> {
|
public class PartItemEssentiaImportBus extends Item implements IPartItem<EssentiaImportBusPart> {
|
||||||
|
|
||||||
public static PartItemEssentiaImportBus create() {
|
public static PartItemEssentiaImportBus create() {
|
||||||
return new PartItemEssentiaImportBus(new Properties().stacksTo(64));
|
return new PartItemEssentiaImportBus(new Item.Properties().stacksTo(64));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartItemEssentiaImportBus(Properties properties) {
|
public PartItemEssentiaImportBus(Properties properties) {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
public class PartItemEssentiaStorageBus extends Item implements IPartItem<EssentiaStorageBusPart> {
|
public class PartItemEssentiaStorageBus extends Item implements IPartItem<EssentiaStorageBusPart> {
|
||||||
|
|
||||||
public static PartItemEssentiaStorageBus create() {
|
public static PartItemEssentiaStorageBus create() {
|
||||||
return new PartItemEssentiaStorageBus(new Properties().stacksTo(64));
|
return new PartItemEssentiaStorageBus(new Item.Properties().stacksTo(64));
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartItemEssentiaStorageBus(Properties properties) {
|
public PartItemEssentiaStorageBus(Properties properties) {
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package thaumicenergistics.common.network;
|
||||||
|
|
||||||
|
import net.minecraft.client.Minecraft;
|
||||||
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||||
|
import net.minecraft.network.codec.StreamCodec;
|
||||||
|
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
|
||||||
|
import net.neoforged.neoforge.network.handling.IPayloadContext;
|
||||||
|
import thaumicenergistics.ThaumicEnergistics;
|
||||||
|
import thaumicenergistics.client.ResearchGateOverlay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务端→客户端 网络包:告知玩家"研究未解锁"拦截发生,并携带具体研究键。
|
||||||
|
*
|
||||||
|
* 服务端在 submitJob 拦截(MixinCraftingService)时发送到对应玩家;
|
||||||
|
* 客户端收到后由 {@link ResearchGateOverlay} 在最上层渲染提示。
|
||||||
|
*/
|
||||||
|
public record ResearchGateErrorPacket(String research) implements CustomPacketPayload {
|
||||||
|
|
||||||
|
public static final Type<ResearchGateErrorPacket> TYPE =
|
||||||
|
new Type<>(ThaumicEnergistics.id("research_gate_error"));
|
||||||
|
|
||||||
|
public static final StreamCodec<RegistryFriendlyByteBuf, ResearchGateErrorPacket> STREAM_CODEC = StreamCodec.of(
|
||||||
|
(buf, pkt) -> buf.writeUtf(pkt.research),
|
||||||
|
buf -> new ResearchGateErrorPacket(buf.readUtf())
|
||||||
|
);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Type<? extends CustomPacketPayload> type() {
|
||||||
|
return TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 客户端处理:记录最近一次研究未解锁拦截的研究键,供渲染层读取。
|
||||||
|
*/
|
||||||
|
public void handleOnClient(IPayloadContext ctx) {
|
||||||
|
ctx.enqueueWork(() -> {
|
||||||
|
// 无论当前在什么界面,记录研究键
|
||||||
|
ResearchGateOverlay.setPendingResearch(this.research);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,5 +49,40 @@ public final class ThENetworking {
|
|||||||
CellWorkbenchSyncPacket.STREAM_CODEC,
|
CellWorkbenchSyncPacket.STREAM_CODEC,
|
||||||
(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(服务端→客户端,研究未解锁拦截提示,携带研究键)
|
||||||
|
registrar.playToClient(
|
||||||
|
ResearchGateErrorPacket.TYPE,
|
||||||
|
ResearchGateErrorPacket.STREAM_CODEC,
|
||||||
|
(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";
|
||||||
|
|
||||||
@@ -164,13 +167,14 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 找到面对中继的隧道端:优先 P2P 输入端,其次任一输出端,最后自身。
|
* 找到面对中继的隧道端:优先 P2P 输入端,其次任一输出端,最后自身。
|
||||||
|
* 注意:内部使用 super.getOutputs() 而非 getOutputs(),避免覆写导致递归。
|
||||||
*/
|
*/
|
||||||
private VisInterfacePart findRelayEnd() {
|
private VisInterfacePart findRelayEnd() {
|
||||||
VisInterfacePart input = getInput();
|
VisInterfacePart input = getInput();
|
||||||
if (input != null && input.getRelay() != null) {
|
if (input != null && input.getRelay() != null) {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
for (VisInterfacePart output : getOutputs()) {
|
for (VisInterfacePart output : super.getOutputs()) {
|
||||||
if (output.getRelay() != null) {
|
if (output.getRelay() != null) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -181,6 +185,25 @@ public class VisInterfacePart extends P2PTunnelPart<VisInterfacePart> implements
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆写输出端列表:半 P2P 场景下(装配器直接按频率查输入,无标准 P2P 输出端),
|
||||||
|
* 若存在有效的中继端(findRelayEnd 非空,含自身面对中继的情况),
|
||||||
|
* 将其视为已链接,使 AE2 P2P 状态显示(Jade/TOP)不再显示"Unlinked"。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<VisInterfacePart> getOutputs() {
|
||||||
|
List<VisInterfacePart> outputs = super.getOutputs();
|
||||||
|
if (!outputs.isEmpty()) {
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
VisInterfacePart relayEnd = findRelayEnd();
|
||||||
|
if (relayEnd != null) {
|
||||||
|
// 含自身面对中继的情况:返回 relayEnd(可能是 this),让 P2P 状态显示已链接
|
||||||
|
return relayEnd == this ? List.of(this) : List.of(relayEnd);
|
||||||
|
}
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
|
||||||
private int drawFromRelay(VisRelayBlockEntity relay, ResourceLocation aspectId, int centivis) {
|
private int drawFromRelay(VisRelayBlockEntity relay, ResourceLocation aspectId, int centivis) {
|
||||||
if (relay == null) {
|
if (relay == null) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -270,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
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public class TransportPermissions implements IThETransportPermissions {
|
|||||||
@Override
|
@Override
|
||||||
public <T extends BlockEntity> boolean addTileToInject(Class<T> c, int cap) { return tileInject.putIfAbsent(c, cap) == null; }
|
public <T extends BlockEntity> boolean addTileToInject(Class<T> c, int cap) { return tileInject.putIfAbsent(c, cap) == null; }
|
||||||
@Override
|
@Override
|
||||||
public OptionalLong getCapacityForTile(Class<? extends BlockEntity> c) {
|
public java.util.OptionalLong getCapacityForTile(Class<? extends BlockEntity> c) {
|
||||||
Integer v = tileExtract.get(c); return v != null ? OptionalLong.of(v) : OptionalLong.empty();
|
Integer v = tileExtract.get(c); return v != null ? java.util.OptionalLong.of(v) : java.util.OptionalLong.empty();
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public boolean canExtract(BlockEntity tile) { return tile != null && tileExtract.containsKey(tile.getClass()); }
|
public boolean canExtract(BlockEntity tile) { return tile != null && tileExtract.containsKey(tile.getClass()); }
|
||||||
|
|||||||
@@ -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 设计)。
|
||||||
@@ -141,7 +158,7 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
super(ModBlockEntities.ARCANE_ASSEMBLER.get(), pos, state);
|
super(ModBlockEntities.ARCANE_ASSEMBLER.get(), pos, state);
|
||||||
|
|
||||||
mainNode = GridHelper.createManagedNode(this, nodeListener)
|
mainNode = GridHelper.createManagedNode(this, nodeListener)
|
||||||
.setVisualRepresentation(ModItems.ARCANE_ASSEMBLER_ITEM.get())
|
.setVisualRepresentation(thaumicenergistics.init.ModItems.ARCANE_ASSEMBLER_ITEM.get())
|
||||||
.setInWorldNode(true)
|
.setInWorldNode(true)
|
||||||
.setTagName("proxy")
|
.setTagName("proxy")
|
||||||
.setFlags(GridFlags.REQUIRE_CHANNEL)
|
.setFlags(GridFlags.REQUIRE_CHANNEL)
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -483,15 +514,13 @@ 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.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;
|
||||||
|
|
||||||
@@ -500,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +551,7 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PatternContainerGroup getCraftingMachineInfo() {
|
public PatternContainerGroup getCraftingMachineInfo() {
|
||||||
ItemStack icon = new ItemStack(ModItems.ARCANE_ASSEMBLER_ITEM.get());
|
ItemStack icon = new ItemStack(thaumicenergistics.init.ModItems.ARCANE_ASSEMBLER_ITEM.get());
|
||||||
return new PatternContainerGroup(
|
return new PatternContainerGroup(
|
||||||
AEItemKey.of(icon),
|
AEItemKey.of(icon),
|
||||||
net.minecraft.network.chat.Component.translatable("block.thaumicenergistics.arcane_assembler"),
|
net.minecraft.network.chat.Component.translatable("block.thaumicenergistics.arcane_assembler"),
|
||||||
@@ -535,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) {
|
||||||
@@ -569,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 源。
|
||||||
@@ -577,7 +605,7 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
*/
|
*/
|
||||||
public void onMemoryCardActivate(net.minecraft.world.entity.player.Player player,
|
public void onMemoryCardActivate(net.minecraft.world.entity.player.Player player,
|
||||||
appeng.api.implementations.items.IMemoryCard memoryCard,
|
appeng.api.implementations.items.IMemoryCard memoryCard,
|
||||||
ItemStack heldItem) {
|
net.minecraft.world.item.ItemStack heldItem) {
|
||||||
Short freq = heldItem.get(appeng.api.ids.AEComponents.EXPORTED_P2P_FREQUENCY);
|
Short freq = heldItem.get(appeng.api.ids.AEComponents.EXPORTED_P2P_FREQUENCY);
|
||||||
if (freq != null) {
|
if (freq != null) {
|
||||||
// 卡上带 P2P 频率(从 vis_interface shift+右键复制来的)→ 设为 vis 源
|
// 卡上带 P2P 频率(从 vis_interface shift+右键复制来的)→ 设为 vis 源
|
||||||
@@ -607,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())) {
|
||||||
@@ -660,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -676,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;
|
||||||
}
|
}
|
||||||
@@ -716,6 +773,7 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
updatePatternSlots();
|
updatePatternSlots();
|
||||||
stalePatterns = true;
|
stalePatterns = true;
|
||||||
calculateVisDiscounts();
|
calculateVisDiscounts();
|
||||||
|
recountSpeedUpgrades();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -751,14 +809,31 @@ public class TileArcaneAssembler extends ThETileBase
|
|||||||
private final AspectList aspects;
|
private final AspectList aspects;
|
||||||
private final ItemStack resultStack;
|
private final ItemStack resultStack;
|
||||||
private final IInput[] inputs;
|
private final IInput[] inputs;
|
||||||
|
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, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArcaneAssemblerPattern(AEItemKey definition, IArcaneRecipe recipe,
|
||||||
|
AspectList aspects, ItemStack resultStack,
|
||||||
|
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.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);
|
||||||
@@ -783,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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -820,5 +902,11 @@ 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。 */
|
||||||
|
@Nullable
|
||||||
|
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,18 +32,75 @@ 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 全量遍历。 */
|
||||||
private List<IAspectStack> cachedEssentiaList = List.of();
|
private List<IAspectStack> cachedEssentiaList = java.util.List.of();
|
||||||
private int essentiaListCacheTimer = 0;
|
private int essentiaListCacheTimer = 0;
|
||||||
private static final int ESSENTIA_LIST_CACHE_TICKS = 20;
|
private static final int ESSENTIA_LIST_CACHE_TICKS = 20;
|
||||||
|
|
||||||
private List<IAspectStack> getCachedEssentiaList() {
|
private List<thaumicenergistics.api.storage.IAspectStack> getCachedEssentiaList() {
|
||||||
if (essentiaListCacheTimer <= 0) {
|
if (essentiaListCacheTimer <= 0) {
|
||||||
essentiaListCacheTimer = ESSENTIA_LIST_CACHE_TICKS;
|
essentiaListCacheTimer = ESSENTIA_LIST_CACHE_TICKS;
|
||||||
cachedEssentiaList = essentiaGrid != null ? essentiaGrid.getEssentiaList() : List.of();
|
cachedEssentiaList = essentiaGrid != null ? essentiaGrid.getEssentiaList() : java.util.List.of();
|
||||||
}
|
}
|
||||||
return cachedEssentiaList;
|
return cachedEssentiaList;
|
||||||
}
|
}
|
||||||
@@ -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 {
|
||||||
@@ -45,7 +47,7 @@ public final class ModBlocks {
|
|||||||
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
||||||
TileArcaneAssembler tile = (TileArcaneAssembler) level.getBlockEntity(pos);
|
TileArcaneAssembler tile = (TileArcaneAssembler) level.getBlockEntity(pos);
|
||||||
return new SimpleMenuProvider(
|
return new SimpleMenuProvider(
|
||||||
(id, inv, p) -> new thaumicenergistics.common.container.ContainerArcaneAssembler(id, inv, tile),
|
(id, inv, p) -> new thaumicenergistics.common.container.ContainerArcaneAssembler(id, inv, tile),
|
||||||
@@ -69,7 +71,7 @@ public final class ModBlocks {
|
|||||||
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
||||||
TileKnowledgeInscriber tile = (TileKnowledgeInscriber) level.getBlockEntity(pos);
|
TileKnowledgeInscriber tile = (TileKnowledgeInscriber) level.getBlockEntity(pos);
|
||||||
return new SimpleMenuProvider(
|
return new SimpleMenuProvider(
|
||||||
(id, inv, p) -> new thaumicenergistics.common.container.ContainerKnowledgeInscriber(id, inv, tile),
|
(id, inv, p) -> new thaumicenergistics.common.container.ContainerKnowledgeInscriber(id, inv, tile),
|
||||||
@@ -92,7 +94,7 @@ public final class ModBlocks {
|
|||||||
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
||||||
TileDistillationPatternEncoder tile = (TileDistillationPatternEncoder) level.getBlockEntity(pos);
|
TileDistillationPatternEncoder tile = (TileDistillationPatternEncoder) level.getBlockEntity(pos);
|
||||||
return new SimpleMenuProvider(
|
return new SimpleMenuProvider(
|
||||||
(id, inv, p) -> new thaumicenergistics.common.container.ContainerDistillationPatternEncoder(id, inv, tile),
|
(id, inv, p) -> new thaumicenergistics.common.container.ContainerDistillationPatternEncoder(id, inv, tile),
|
||||||
@@ -110,7 +112,7 @@ public final class ModBlocks {
|
|||||||
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
return net.minecraft.world.ItemInteractionResult.SUCCESS;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
protected MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
protected net.minecraft.world.MenuProvider getMenuProvider(BlockState state, net.minecraft.world.level.Level level, BlockPos pos) {
|
||||||
net.minecraft.world.level.block.entity.BlockEntity be = level.getBlockEntity(pos);
|
net.minecraft.world.level.block.entity.BlockEntity be = level.getBlockEntity(pos);
|
||||||
if (be instanceof TileEssentiaCellWorkbench workbench) {
|
if (be instanceof TileEssentiaCellWorkbench workbench) {
|
||||||
return new SimpleMenuProvider((id, inv, p) -> new thaumicenergistics.common.container.ContainerEssentiaCellWorkbench(id, inv, workbench),
|
return new SimpleMenuProvider((id, inv, p) -> new thaumicenergistics.common.container.ContainerEssentiaCellWorkbench(id, inv, workbench),
|
||||||
@@ -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")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ public class ArcaneCraftingTransferInfo
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
|
public List<Slot> getRecipeSlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
|
||||||
return menu.getSlots(SlotSemantics.CRAFTING_GRID);
|
return menu.getSlots(appeng.menu.SlotSemantics.CRAFTING_GRID);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
|
public List<Slot> getInventorySlots(ContainerArcaneCraftingTerminal menu, RecipeHolder<CraftingRecipe> recipe) {
|
||||||
return menu.getSlots(SlotSemantics.PLAYER_INVENTORY);
|
return menu.getSlots(appeng.menu.SlotSemantics.PLAYER_INVENTORY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-45
@@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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.CallbackInfo;
|
||||||
|
|
||||||
|
import thaumicenergistics.client.ResearchGateOverlay;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拦截 CraftConfirmScreen 打开 CPU 忙错误屏。
|
||||||
|
*
|
||||||
|
* 研究未解锁拦截会触发 AE2 的 CPU_BUSY 错误屏;本 Mixin 在错误屏被 switchToScreen 之前
|
||||||
|
* (updateBeforeRender HEAD)检测到研究拦截(ResearchGateOverlay 已有网络包写入的研究键),
|
||||||
|
* 取消原渲染逻辑,避免打开误导性的"CPU 忙"窗口。
|
||||||
|
* 提示文字由 ResearchGateOverlay 在无 GUI 遮挡时绘制。
|
||||||
|
*/
|
||||||
|
@Mixin(targets = "appeng.client.gui.me.crafting.CraftConfirmScreen", remap = false)
|
||||||
|
public abstract class MixinCraftConfirmScreen {
|
||||||
|
|
||||||
|
@Inject(method = "updateBeforeRender()V", at = @At("HEAD"), cancellable = true, remap = false)
|
||||||
|
private void thaumicenergistics$blockCpuBusyScreen(CallbackInfo ci) {
|
||||||
|
// 网络包已写入研究键 = 本次失败是研究拦截,而非真实 CPU 忙碌
|
||||||
|
// 使用 submitError 状态而非时间戳:只要错误未被玩家清除就持续拦截,避免过期后弹窗
|
||||||
|
if (ResearchGateOverlay.hasResearchGateError()) {
|
||||||
|
ci.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user