diff --git a/.gitignore b/.gitignore index 6860eca..1b699c4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ progress.md /.reasonix/ /.tmp_*/ __pycache__/ +/Issue-改善物品方块扫描要素.md +/路径.md diff --git a/gradle.properties b/gradle.properties index 72f65ac..f6d74bf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Project -mod_version=2.0.1 +mod_version=2.1.1 mod_group=thaumicenergistics # Mod diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..f3b75f3 --- /dev/null +++ b/gradlew @@ -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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -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 diff --git a/src/main/java/thaumicenergistics/ThaumicEnergistics.java b/src/main/java/thaumicenergistics/ThaumicEnergistics.java index b8b1669..b20153e 100644 --- a/src/main/java/thaumicenergistics/ThaumicEnergistics.java +++ b/src/main/java/thaumicenergistics/ThaumicEnergistics.java @@ -63,6 +63,8 @@ public final class ThaumicEnergistics { thaumicenergistics.common.network.ThENetworking.init(modBus); + thaumicenergistics.common.blocks.ThEWrenchHandler.register(); + modBus.addListener(this::registerCapabilities); PartModels.registerModels( @@ -147,7 +149,14 @@ public final class ThaumicEnergistics { event.register(ModMenuTypes.ESSENTIA_CELL_WORKBENCH.get(), GuiEssentiaCellWorkbench::new); event.register(ModMenuTypes.EVC.get(), GuiEssentiaVibrationChamber::new); event.register(ModMenuTypes.ARCANE_ASSEMBLER.get(), GuiArcaneAssembler::new); - event.register(ModMenuTypes.ESSENTIA_TERMINAL.get(), GuiEssentiaCellTerminal::new); + event.register(ModMenuTypes.ESSENTIA_TERMINAL.get(), + (EssentiaTerminalMenu menu, Inventory inv, Component title) + -> new GuiEssentiaTerminal(menu, inv, title, + StyleManager.loadStyleDoc("/screens/terminals/terminal.json"))); + event.register(ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get(), + (EssentiaTerminalMenu menu, Inventory inv, Component title) + -> new GuiEssentiaTerminal(menu, inv, title, + StyleManager.loadStyleDoc("/screens/terminals/wireless_terminal.json"))); event.register(ModMenuTypes.KNOWLEDGE_INSCRIBER.get(), GuiKnowledgeInscriber::new); event.register(ModMenuTypes.DISTILLATION_ENCODER.get(), GuiDistillationPatternEncoder::new); diff --git a/src/main/java/thaumicenergistics/common/blocks/OrientableEntityBlock.java b/src/main/java/thaumicenergistics/common/blocks/OrientableEntityBlock.java index bfde4af..c29f4fb 100644 --- a/src/main/java/thaumicenergistics/common/blocks/OrientableEntityBlock.java +++ b/src/main/java/thaumicenergistics/common/blocks/OrientableEntityBlock.java @@ -1,10 +1,12 @@ package thaumicenergistics.common.blocks; +import java.util.List; + import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.world.item.context.BlockPlaceContext; -import net.minecraft.world.InteractionHand; import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.InteractionHand; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; @@ -14,10 +16,8 @@ import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.DirectionProperty; +import net.minecraft.world.level.storage.loot.LootParams; import net.minecraft.world.phys.BlockHitResult; -import thaumcraft.api.aspects.Aspect; -import thaumcraft.api.aspects.AspectList; -import thaumcraft.api.aspects.IEssentiaContainerItem; import thaumicenergistics.common.tiles.ThETileBase; import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber; @@ -43,20 +43,16 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock @Override public abstract BlockEntity newBlockEntity(BlockPos pos, BlockState state); + /** + * 谐振仓:通配吸力模式,不再需要安瓿瓶切换燃料类型。 + * 放行默认交互(若后续添加 GUI 则走 getMenuProvider)。 + */ @Override protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { if (level.isClientSide()) return ItemInteractionResult.SUCCESS; BlockEntity be = level.getBlockEntity(pos); if (!(be instanceof TileEssentiaVibrationChamber chamber)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; - // 检查手持物品是否为源质安瓿瓶 - if (!(stack.getItem() instanceof IEssentiaContainerItem container)) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; - AspectList aspects = container.getAspects(stack); - if (aspects == null || aspects.size() == 0) return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; - Aspect aspect = aspects.getAspects()[0]; - if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) { - chamber.setSuctionType(aspect); - return ItemInteractionResult.SUCCESS; - } + // 通配吸力:无需切换,直接放行 return ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; } @@ -68,4 +64,12 @@ public abstract class OrientableEntityBlock extends Block implements EntityBlock } super.onRemove(state, level, pos, newState, moved); } + + /** + * 挖掘掉落方块自身(不导出方块实体 NBT)。 + */ + @Override + public List getDrops(BlockState state, LootParams.Builder builder) { + return List.of(new ItemStack(this)); + } } diff --git a/src/main/java/thaumicenergistics/common/blocks/ThEBaseEntityBlock.java b/src/main/java/thaumicenergistics/common/blocks/ThEBaseEntityBlock.java index 0c0bab4..b07e911 100644 --- a/src/main/java/thaumicenergistics/common/blocks/ThEBaseEntityBlock.java +++ b/src/main/java/thaumicenergistics/common/blocks/ThEBaseEntityBlock.java @@ -1,13 +1,21 @@ package thaumicenergistics.common.blocks; +import java.util.List; + import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.EntityBlock; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.BlockEntityTicker; import net.minecraft.world.level.block.entity.BlockEntityType; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.Level; +import net.minecraft.world.level.storage.loot.LootParams; +import net.minecraft.world.phys.BlockHitResult; import thaumicenergistics.common.tiles.TileEssentiaProvider; import thaumicenergistics.common.tiles.TileEssentiaVibrationChamber; import thaumicenergistics.common.tiles.TileInfusionProvider; @@ -36,6 +44,24 @@ public abstract class ThEBaseEntityBlock extends Block implements EntityBlock { return (BlockEntityTicker) 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 getDrops(BlockState state, LootParams.Builder builder) { + return List.of(new ItemStack(this)); + } + @Override public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { if (!state.is(newState.getBlock())) { diff --git a/src/main/java/thaumicenergistics/common/container/ContainerArcaneAssembler.java b/src/main/java/thaumicenergistics/common/container/ContainerArcaneAssembler.java index b03f961..002c651 100644 --- a/src/main/java/thaumicenergistics/common/container/ContainerArcaneAssembler.java +++ b/src/main/java/thaumicenergistics/common/container/ContainerArcaneAssembler.java @@ -13,7 +13,7 @@ import net.neoforged.neoforge.items.SlotItemHandler; import thaumcraft.api.IVisDiscountGear; import thaumcraft.api.IWarpingGear; import thaumcraft.api.aspects.Aspect; -import thaumicenergistics.ThaumicEnergistics; +import thaumicenergistics.common.container.slot.ReadOnlySlot; import thaumicenergistics.common.tiles.TileArcaneAssembler; import thaumicenergistics.init.ModMenuTypes; @@ -160,16 +160,12 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu { } }); - // ===== 4. 3×7 配方槽(只读展示) ===== + // ===== 4. 3×7 配方槽(只读展示,AE2 式防整理) ===== for (int r = 0; r < PATTERN_ROWS; r++) for (int c = 0; c < PATTERN_COLS; c++) { int invIdx = TileArcaneAssembler.PATTERN_SLOT_START + r * PATTERN_COLS + c; - addSlot(new SlotItemHandler(machineHandler, invIdx, - PATTERN_X + c * 18, PATTERN_Y + r * 18) { - @Override public boolean mayPlace(ItemStack s) { return false; } - @Override public boolean mayPickup(Player p) { return false; } - @Override public int getMaxStackSize() { return 1; } - }); + addSlot(new ReadOnlySlot(machineHandler, invIdx, + PATTERN_X + c * 18, PATTERN_Y + r * 18)); } // ===== 5. 4张AE2速度卡 ===== @@ -182,11 +178,8 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu { }); } - // ===== 6. 目标产物输出槽(只读展示) ===== - addSlot(new SlotItemHandler(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y) { - @Override public boolean mayPlace(ItemStack s) { return false; } - @Override public boolean mayPickup(Player p) { return false; } - }); + // ===== 6. 目标产物输出槽(只读展示,AE2 式防整理) ===== + addSlot(new ReadOnlySlot(machineHandler, TileArcaneAssembler.TARGET_SLOT, TARGET_X, TARGET_Y)); // ===== 7. 4个护甲折扣槽 ===== for (int i = 0; i < ARMOR_COUNT; i++) { @@ -202,9 +195,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu { @Override public boolean mayPlace(ItemStack s) { if (s.getItem() instanceof IVisDiscountGear) return true; if (s.getItem() instanceof IWarpingGear) return true; - if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem - && eqItem.getEquipmentSlot() == eq) return true; - return s.canEquip(eq, null); + if (s.getItem() instanceof net.minecraft.world.item.Equipable eqItem) + return eqItem.getEquipmentSlot() == eq; + return false; } @Override public void setChanged() { super.setChanged(); @@ -220,13 +213,9 @@ public class ContainerArcaneAssembler extends AbstractContainerMenu { this(id, inv, resolveTile(inv, buf)); } - /** 临时诊断:确认客户端能否拿到 tile(定位后删除) */ private static TileArcaneAssembler resolveTile(Inventory inv, RegistryFriendlyByteBuf buf) { var pos = buf.readBlockPos(); - var be = inv.player.level().getBlockEntity(pos); - ThaumicEnergistics.LOG.info("[AA-DEBUG] client ctor: pos={}, be={}", - pos, be == null ? "null" : be.getClass().getSimpleName()); - return (TileArcaneAssembler) be; + return (TileArcaneAssembler) inv.player.level().getBlockEntity(pos); } private ItemStackHandler wrapMachineInventory() { diff --git a/src/main/java/thaumicenergistics/common/container/ContainerArcaneCraftingTerminal.java b/src/main/java/thaumicenergistics/common/container/ContainerArcaneCraftingTerminal.java index fbffee4..59aaddd 100644 --- a/src/main/java/thaumicenergistics/common/container/ContainerArcaneCraftingTerminal.java +++ b/src/main/java/thaumicenergistics/common/container/ContainerArcaneCraftingTerminal.java @@ -33,9 +33,11 @@ import appeng.me.storage.LinkStatusRespectingInventory; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.AspectList; +import thaumcraft.api.wands.WandCap; import net.minecraft.world.item.crafting.Ingredient; import thaumcraft.common.items.TCFunctionalItems; import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager; +import thaumcraft.common.vis.TCVisHelper; import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.common.container.slot.ArcaneCraftingResultSlot; import thaumicenergistics.common.parts.ArcaneCraftingTerminalPart; @@ -55,6 +57,34 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC */ private boolean isCraftingResultBeingTaken = false; + /** vis 恢复检测节流:每 10 tick(0.5 秒)检查一次,避免持续遍历配方。 */ + private static final int VIS_RECOVERY_CHECK_INTERVAL = 10; + private int visRecoveryTickCounter = 0; + + @Override + public void broadcastChanges() { + super.broadcastChanges(); + // vis 充能恢复后自动重算结果槽:结果槽空 + 奥术配方有效 + vis 已足够 → 重新显示产物。 + // 节流:仅"待恢复"状态每 10 tick 检查一次(findMatchingArcaneRecipe 只在满足时执行) + var player = getPlayerInventory().player; + if (player.level().isClientSide() || !hasValidRecipe || !resultSlot.getItem().isEmpty()) { + return; + } + if (++visRecoveryTickCounter < VIS_RECOVERY_CHECK_INTERVAL) { + return; + } + visRecoveryTickCounter = 0; + var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer(); + ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player); + if (!arcaneResult.isEmpty()) { + AspectList aspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player); + if (aspects != null && hasEnoughVis(aspects)) { + resultSlot.setRequiredAspects(aspects); + resultSlot.setDisplayedCraftingOutput(arcaneResult); + } + } + } + private final ISegmentedInventory craftingInventoryHost; private final CraftingMatrixSlot[] craftingSlots = new CraftingMatrixSlot[9]; private final ArcaneCraftingResultSlot resultSlot; @@ -76,9 +106,9 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC } var linkStatusInventory = new LinkStatusRespectingInventory(host.getInventory(), this::getLinkStatus); - this.resultSlot = new ArcaneCraftingResultSlot(getPlayerInventory().player, craftingGridInv); - this.resultSlot.setRefreshCallback(() -> updateCurrentRecipeAndOutput(false)); - this.resultSlot.setContainer(this); + this.resultSlot = new ArcaneCraftingResultSlot( + getPlayerInventory().player, this, craftingGridInv, craftingGridInv, + linkStatusInventory, this.getEnergySource(), this.getActionSource()); this.addSlot(this.resultSlot, SlotSemantics.CRAFTING_RESULT); var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND); @@ -88,7 +118,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC return 1; } - @Override public boolean mayPlace(ItemStack stack) { //只允许 TCFunctionalItems.WandCastingItem 类型的物品放入 @@ -145,6 +174,26 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC private static final int CENTIVIS = 100; + /** + * 复刻 TC port {@code WandCastingItem.consumptionModifier}:法杖 cap 折扣 − 玩家装备/饰品折扣 − 法杖折扣。 + * 仅用于显示(GUI 所需 vis),实际扣费由 consumeVisCost 内部同公式处理。 + */ + private float getConsumptionModifier(Aspect aspect, boolean crafting) { + var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND); + var wandStack = wandInv.getStackInSlot(0); + if (wandStack.isEmpty() || !(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) { + return 1.0f; + } + float modifier = wand.consumptionModifier(wandStack, getPlayerInventory().player, aspect, crafting); + return modifier; + } + + /** 折扣后的 vis 成本(GUI 显示用)。 */ + private int getDiscountedVisCost(Aspect aspect, int rawCost) { + if (rawCost <= 0 || aspect == null) return 0; + return Math.max(0, Math.round(rawCost * getConsumptionModifier(aspect, true))); + } + private boolean hasEnoughVis(AspectList cost) { ThaumicEnergistics.LOG.debug("[V] hasEnoughVis: cost={}", cost); if (cost == null || cost.isEmpty()) { @@ -172,7 +221,6 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC } private void updateCurrentRecipeAndOutput(boolean forceUpdate) { - ThaumicEnergistics.LOG.debug("[R] === updateCurrentRecipeAndOutput forceUpdate={} ===", forceUpdate); var player = getPlayerInventory().player; var level = player.level(); @@ -183,27 +231,20 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC var craftContainer = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_CRAFTING).toContainer(); ItemStack arcaneResult = ThaumcraftCraftingManager.findMatchingArcaneRecipe(craftContainer, player); - ThaumicEnergistics.LOG.debug("[R] findMatchingArcaneRecipe result={}", arcaneResult); if (!arcaneResult.isEmpty()) { requiredAspects = ThaumcraftCraftingManager.findMatchingArcaneRecipeAspects(craftContainer, player); - ThaumicEnergistics.LOG.debug("[R] ARCANE MATCH! aspects={}, result={}", requiredAspects, arcaneResult); foundArcane = true; if (hasEnoughVis(requiredAspects)) { - ThaumicEnergistics.LOG.debug("[R] Vis OK → showing result"); resultSlot.setDisplayedCraftingOutput(arcaneResult); } else { - ThaumicEnergistics.LOG.debug("[R] Vis INSUFFICIENT → result empty, but recipe recognized"); resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY); } - } else { - ThaumicEnergistics.LOG.debug("[R] No arcane recipe match"); } } if (!foundArcane) { - ThaumicEnergistics.LOG.debug("[R] No arcane match, trying vanilla..."); var testItems = new ArrayList(this.craftingSlots.length); for (var craftingSlot : this.craftingSlots) { testItems.add(craftingSlot.getItem().copy()); @@ -212,10 +253,8 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC var recipeHolder = level.getRecipeManager().getRecipeFor(RecipeType.CRAFTING, testInput, level); if (recipeHolder.isPresent()) { ItemStack result = recipeHolder.get().value().assemble(testInput, level.registryAccess()); - ThaumicEnergistics.LOG.debug("[R] Vanilla match: {}, setting result slot", result); resultSlot.setDisplayedCraftingOutput(result); } else { - ThaumicEnergistics.LOG.debug("[R] No vanilla match either, clearing result slot"); // 客户端不清空产物槽,@GuiSync 会同步服务端的正确状态 if (!level.isClientSide()) { resultSlot.setDisplayedCraftingOutput(ItemStack.EMPTY); @@ -225,9 +264,7 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC if (foundArcane) { // 服务端设置 @GuiSync 字段;客户端不应覆盖来自服务端的同步值 - ThaumicEnergistics.LOG.debug("[R] foundArcane branch: isClientSide={}", level.isClientSide()); if (level.isClientSide()) { - ThaumicEnergistics.LOG.debug("[R] Client: keeping server-synced values, not overriding"); return; } this.hasValidRecipe = true; @@ -235,32 +272,27 @@ public class ContainerArcaneCraftingTerminal extends MEStorageMenu implements IC var wandInv = this.craftingInventoryHost.getSubInventory(ArcaneCraftingTerminalPart.INV_WAND); var ws = wandInv.getStackInSlot(0); resultSlot.setWandStack(ws); - ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=true, wandStack={}", ws); if (requiredAspects != null && requiredAspects.size() > 0) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : requiredAspects.aspects.entrySet()) { if (!sb.isEmpty()) sb.append(","); - sb.append(entry.getKey().getTag()).append("=").append(entry.getValue()); + // 存折扣后数值(GUI 显示实际所需 vis,含法杖 + 装备 + 饰品减免) + sb.append(entry.getKey().getTag()).append("=") + .append(getDiscountedVisCost(entry.getKey(), entry.getValue())); } this.requiredAspectsData = sb.toString(); - ThaumicEnergistics.LOG.debug("[R] aspectsData={}", this.requiredAspectsData); } else { this.requiredAspectsData = ""; - ThaumicEnergistics.LOG.debug("[R] aspectsData=empty (no aspects)"); } } else { - ThaumicEnergistics.LOG.debug("[R] else branch: isClientSide={}, levelClass={}", - level.isClientSide(), level.getClass().getSimpleName()); if (level.isClientSide()) { - ThaumicEnergistics.LOG.debug("[R] Client: not overriding synced values (no arcane recipe on client)"); return; } this.requiredAspectsData = ""; this.hasValidRecipe = false; resultSlot.setRequiredAspects(null); resultSlot.setWandStack(ItemStack.EMPTY); - ThaumicEnergistics.LOG.debug("[R] SET state: hasValidRecipe=false (no arcane recipe)"); } } diff --git a/src/main/java/thaumicenergistics/common/container/ContainerDistillationPatternEncoder.java b/src/main/java/thaumicenergistics/common/container/ContainerDistillationPatternEncoder.java index d0c76ac..f61ffae 100644 --- a/src/main/java/thaumicenergistics/common/container/ContainerDistillationPatternEncoder.java +++ b/src/main/java/thaumicenergistics/common/container/ContainerDistillationPatternEncoder.java @@ -24,6 +24,7 @@ import thaumcraft.api.aspects.AspectList; import thaumcraft.common.research.TCResearchManager; import thaumcraft.common.research.ThaumometerScanManager; import thaumicenergistics.ThaumicEnergistics; +import thaumicenergistics.common.container.slot.ReadOnlySlot; import thaumicenergistics.common.integration.appeng.AEssentiaKey; import thaumicenergistics.common.integration.tc.TCReflection; import thaumicenergistics.common.network.DistillationEncoderSetSourceC2SPacket; @@ -98,20 +99,10 @@ public class ContainerDistillationPatternEncoder extends AbstractContainerMenu { for (int i = 0; i < SLOT_ASPECTS_COUNT; i++) { int y = SLOT_ASPECTS_Y + i * 18; - addSlot(new SlotItemHandler(aspectInventory, i, SLOT_ASPECTS_X, y) { - @Override - public boolean mayPlace(ItemStack s) { return false; } - @Override - public boolean mayPickup(Player p) { return false; } - }); + addSlot(new ReadOnlySlot(aspectInventory, i, SLOT_ASPECTS_X, y)); } - addSlot(new SlotItemHandler(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y) { - @Override - public boolean mayPlace(ItemStack s) { return false; } - @Override - public boolean mayPickup(Player p) { return false; } - }); + addSlot(new ReadOnlySlot(aspectInventory, SLOT_ASPECTS_COUNT, SLOT_SELECTED_X, SLOT_SELECTED_Y)); if (encoder != null) { // 源物品槽:ghost 模板(背包拖入不消耗——实际物品还在手里;空手点击清空;JEI 拖拽设置模板) diff --git a/src/main/java/thaumicenergistics/common/container/ContainerEssentiaCellTerminal.java b/src/main/java/thaumicenergistics/common/container/ContainerEssentiaCellTerminal.java deleted file mode 100644 index 9a326d0..0000000 --- a/src/main/java/thaumicenergistics/common/container/ContainerEssentiaCellTerminal.java +++ /dev/null @@ -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); } -} diff --git a/src/main/java/thaumicenergistics/common/container/ContainerKnowledgeInscriber.java b/src/main/java/thaumicenergistics/common/container/ContainerKnowledgeInscriber.java index eb19d33..02c97f4 100644 --- a/src/main/java/thaumicenergistics/common/container/ContainerKnowledgeInscriber.java +++ b/src/main/java/thaumicenergistics/common/container/ContainerKnowledgeInscriber.java @@ -25,6 +25,7 @@ import thaumcraft.api.crafting.ShapedArcaneRecipe; import thaumcraft.api.crafting.ShapelessArcaneRecipe; import thaumcraft.common.research.TCResearchManager; import thaumicenergistics.common.container.slot.GhostSlot; +import thaumicenergistics.common.container.slot.ReadOnlySlot; import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern; import thaumicenergistics.common.inventory.HandlerKnowledgeCore; import thaumicenergistics.common.network.KnowledgeInscriberGhostSlotPacket; @@ -121,12 +122,7 @@ public class ContainerKnowledgeInscriber extends AbstractContainerMenu { initPatternSlots(); initCraftingSlots(); - addSlot(new SlotItemHandler(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y) { - @Override - public boolean mayPlace(ItemStack s) { return false; } - @Override - public boolean mayPickup(Player p) { return false; } - }); + addSlot(new ReadOnlySlot(resultInventory, 0, RESULT_SLOT_X, RESULT_SLOT_Y)); saveStateSlot = addDataSlot(DataSlot.standalone()); saveStateSlot.set(0); diff --git a/src/main/java/thaumicenergistics/common/container/GuiArcaneAssembler.java b/src/main/java/thaumicenergistics/common/container/GuiArcaneAssembler.java index d9c0dbd..ee070cc 100644 --- a/src/main/java/thaumicenergistics/common/container/GuiArcaneAssembler.java +++ b/src/main/java/thaumicenergistics/common/container/GuiArcaneAssembler.java @@ -7,9 +7,10 @@ import net.minecraft.resources.ResourceLocation; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.ContainerData; import thaumicenergistics.ThaumicEnergistics; +import org.anti_ad.mc.ipn.api.IPNIgnore; /** - * 奥术装配器 GUI。 + * 奥术装配器 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。 * * 贴图 arcane_assembler.png = 256×256 画布,包含4块不连续区域: * 第1块 主矩形:(0,0) → 176×197 @@ -20,6 +21,7 @@ import thaumicenergistics.ThaumicEnergistics; * 仿1.7.10纹理裁剪方式:GUI分块blit,bar区域不随纹理整体绘制, * 而是先画暗色背景,再从纹理Y=202处裁剪满bar按百分比叠加。 */ +@IPNIgnore public class GuiArcaneAssembler extends AbstractContainerScreen { static final ResourceLocation TEX = ResourceLocation.fromNamespaceAndPath( diff --git a/src/main/java/thaumicenergistics/common/container/GuiArcaneCraftingTerminal.java b/src/main/java/thaumicenergistics/common/container/GuiArcaneCraftingTerminal.java index b0c6310..fe8e4ef 100644 --- a/src/main/java/thaumicenergistics/common/container/GuiArcaneCraftingTerminal.java +++ b/src/main/java/thaumicenergistics/common/container/GuiArcaneCraftingTerminal.java @@ -11,17 +11,13 @@ import appeng.client.gui.style.ScreenStyle; import java.util.Map; -public class GuiArcaneCraftingTerminal extends MEStorageScreen { +import org.anti_ad.mc.ipn.api.IPNIgnore; - /** 6 个要素图标位置(左上角坐标) */ - private static final int[][] ASPECT_POSITIONS = { - {10, 95}, // 左列 第1行 - {10, 112}, // 左列 第2行 - {10, 128}, // 左列 第3行 - {80, 95}, // 右列 第1行 - {80, 112}, // 右列 第2行 - {80, 128}, // 右列 第3行 - }; +/** + * 奥术合成终端 GUI。标 @IPNIgnore:IPN 一键整理完全跳过此界面(不渲染排序按钮、不执行整理)。 + */ +@IPNIgnore +public class GuiArcaneCraftingTerminal extends MEStorageScreen { private static final int ICON_SIZE = 14; @@ -30,9 +26,6 @@ public class GuiArcaneCraftingTerminal extends MEStorageScreen aspectMap = this.menu.getParsedAspects(); if (aspectMap.isEmpty()) return; - // 获取合成结果槽的 Y 位置作为锚点(槽位置不变,首次计算后缓存,避免每帧遍历) - int anchorY; - if (cachedAnchorY < 0) { - for (var slot : this.menu.slots) { - // CRAFTING_RESULT 槽在 style JSON 中 bottom:140 - if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) { - cachedAnchorY = slot.y; - break; - } + // 锚点取合成结果槽的 Y:圆环紧贴结果槽左侧,窗口缩放时随槽位一起移动 + int anchorY = -1; + for (var slot : this.menu.slots) { + if (slot instanceof appeng.menu.slot.AppEngCraftingSlot) { + anchorY = slot.y; + break; } - // 若找不到锚点则回退到硬编码值 - if (cachedAnchorY < 0) cachedAnchorY = 95; } - anchorY = cachedAnchorY; + if (anchorY < 0) anchorY = 95; - // 要素圆圈 Y = 锚点 + 偏移(-16/1/17,对应纹理 95/112/128) + // 两列三行:左列 x=10,右列 x=80;行偏移对应三行圆环 int[] aspectOffsets = { -16, 1, 17 }; int index = 0; diff --git a/src/main/java/thaumicenergistics/common/container/GuiEssentiaCellTerminal.java b/src/main/java/thaumicenergistics/common/container/GuiEssentiaCellTerminal.java deleted file mode 100644 index b9faf30..0000000 --- a/src/main/java/thaumicenergistics/common/container/GuiEssentiaCellTerminal.java +++ /dev/null @@ -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 { - 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); } -} diff --git a/src/main/java/thaumicenergistics/common/container/slot/ArcaneCraftingResultSlot.java b/src/main/java/thaumicenergistics/common/container/slot/ArcaneCraftingResultSlot.java index 1f49da8..8d92a72 100644 --- a/src/main/java/thaumicenergistics/common/container/slot/ArcaneCraftingResultSlot.java +++ b/src/main/java/thaumicenergistics/common/container/slot/ArcaneCraftingResultSlot.java @@ -4,37 +4,44 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import appeng.api.inventories.InternalInventory; -import appeng.menu.slot.AppEngCraftingSlot; +import appeng.api.networking.energy.IEnergySource; +import appeng.api.networking.security.IActionSource; +import appeng.api.storage.MEStorage; +import appeng.helpers.ICraftingGridMenu; +import appeng.helpers.InventoryAction; +import appeng.menu.slot.CraftingTermSlot; +import appeng.util.inv.PlayerInternalInventory; import thaumcraft.api.aspects.Aspect; import thaumcraft.api.aspects.AspectList; import thaumcraft.common.items.TCFunctionalItems; -import thaumcraft.common.lib.crafting.ThaumcraftCraftingManager; import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal; /** - * 奥术合成结果槽:在取走合成结果时消耗法杖要素。 - * 参考 TC4 1.21.1 ArcaneWorkbenchMenu.onResultTaken + consumeVis。 + * 奥术合成结果槽:继承 AE2 CraftingTermSlot 使客户端识别(shift/右键/space 合成自动生效), + * 并保留 vis 消耗检查(mayPickup)与取走时 vis 扣除 + 自动补货(onTake)。 */ -public class ArcaneCraftingResultSlot extends AppEngCraftingSlot { +public class ArcaneCraftingResultSlot extends CraftingTermSlot { /** TC4 内部使用 centivis,1 vis = 100 centivis */ private static final int CENTIVIS = 100; private AspectList requiredAspects = null; private ItemStack wandStack = ItemStack.EMPTY; - private Runnable refreshCallback = null; private ContainerArcaneCraftingTerminal container = null; + private final MEStorage storage; + private final IEnergySource energySrc; + private final IActionSource mySrc; + private final InternalInventory craftingGrid; - public ArcaneCraftingResultSlot(Player player, InternalInventory craftingGridInv) { - super(player, craftingGridInv); - } - - public void setContainer(ContainerArcaneCraftingTerminal container) { + public ArcaneCraftingResultSlot(Player player, ContainerArcaneCraftingTerminal container, + InternalInventory craftingGridInv, InternalInventory secondMatrix, + MEStorage storage, IEnergySource energySrc, IActionSource mySrc) { + super(player, mySrc, energySrc, storage, craftingGridInv, secondMatrix, (ICraftingGridMenu) container); this.container = container; - } - - public void setRefreshCallback(Runnable callback) { - this.refreshCallback = callback; + this.storage = storage; + this.energySrc = energySrc; + this.mySrc = mySrc; + this.craftingGrid = craftingGridInv; } public void setRequiredAspects(AspectList aspects) { @@ -47,40 +54,177 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot { @Override public boolean mayPickup(Player player) { - if (requiredAspects == null || requiredAspects.isEmpty()) { - return super.mayPickup(player); + // AE2 原版:结果槽不可 vanilla 点按取走,所有取走走 doClick(含 vis 检查) + return false; + } + + @Override + public void doClick(InventoryAction action, Player who) { + // vis 检查:奥术配方需法杖有足够 vis,否则不合成 + if (requiredAspects != null && !requiredAspects.isEmpty()) { + if (wandStack.isEmpty()) { + return; + } + if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) { + return; + } + AspectList centivisCost = new AspectList(); + for (Aspect aspect : requiredAspects.aspects()) { + centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS); + } + if (!wand.consumeVisCost(wandStack, who, centivisCost, false, true)) { + return; + } + } + + + // 奥术配方(非 vanilla CRAFTING)不走 AE2 craftItem(findRecipe 只认 vanilla) + // 走自定义合成:从合成格读配方 → 产物 + 消耗材料 + 从网络补货 + if (container != null && container.hasValidRecipe) { + doArcaneCraft(action, who); + return; } - if (wandStack.isEmpty()) return false; - if (!(wandStack.getItem() instanceof TCFunctionalItems.WandCastingItem wand)) { - return false; + // 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 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; } - AspectList centivisCost = new AspectList(); - for (Aspect aspect : requiredAspects.aspects()) { - centivisCost.add(aspect, requiredAspects.amount(aspect) * CENTIVIS); + 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(); + 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++; } - return wand.consumeVisCost(wandStack, player, centivisCost, false, true); + + // 触发容器刷新(重算结果槽) + container.slotsChanged(grid.toContainer()); } @Override public void onTake(Player player, ItemStack stack) { - // 记录消耗前各合成格中的物品类型,用于消耗后从 ME 网络补充 - java.util.Map beforeItems = new java.util.HashMap<>(); - if (container != null) { - var grid = container.getCraftingMatrix(); - for (int i = 0; i < grid.size(); i++) { - var s = grid.getStackInSlot(i); - if (!s.isEmpty()) { - beforeItems.put(i, s.copy()); - } - } - } - if (container != null) { container.setCraftingResultBeingTaken(true); } try { + // 记录消耗前各合成格物品,取走后从 ME 存储自动补回(AE2 终端行为) + java.util.Map beforeItems = new java.util.HashMap<>(); + var grid = craftingGrid; + for (int i = 0; i < grid.size(); i++) { + var s = grid.getStackInSlot(i); + if (!s.isEmpty()) beforeItems.put(i, s.copy()); + } + super.onTake(player, stack); if (requiredAspects != null && !requiredAspects.isEmpty() && !wandStack.isEmpty()) { @@ -93,28 +237,22 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot { } } - // 从 ME 网络自动补充消耗掉的合成材料 - if (container != null && !beforeItems.isEmpty()) { - var grid = container.getCraftingMatrix(); - var node = container.getGridNode(); - if (node != null && node.getGrid() != null) { - var storage = node.getGrid().getStorageService().getInventory(); - var energy = container.getEnergySource(); - var actionSource = container.getActionSource(); + // 自动补货:合成格材料被消耗后从 ME 存储补回 + if (!beforeItems.isEmpty()) { + if (storage != null && energySrc != null && mySrc != null) { for (var entry : beforeItems.entrySet()) { int slot = entry.getKey(); var beforeItem = entry.getValue(); var currentItem = grid.getStackInSlot(slot); - // 如果该格物品变少了(被消耗了),从网络提取补充 if (currentItem.getCount() < beforeItem.getCount()) { long needed = beforeItem.getCount() - currentItem.getCount(); var key = appeng.api.stacks.AEItemKey.of(beforeItem); if (key != null) { long extracted = appeng.api.storage.StorageHelper.poweredExtraction( - energy, storage, key, needed, actionSource); + energySrc, storage, key, needed, mySrc); if (extracted > 0) { - var toPlace = beforeItem.copyWithCount((int) (currentItem.getCount() + extracted)); - grid.setItemDirect(slot, toPlace); + grid.setItemDirect(slot, beforeItem.copyWithCount( + (int) (currentItem.getCount() + extracted))); } } } @@ -126,8 +264,6 @@ public class ArcaneCraftingResultSlot extends AppEngCraftingSlot { container.setCraftingResultBeingTaken(false); } } - if (refreshCallback != null) { - refreshCallback.run(); - } } + } diff --git a/src/main/java/thaumicenergistics/common/features/RecipeRegistration.java b/src/main/java/thaumicenergistics/common/features/RecipeRegistration.java index ddcd26f..57d0a20 100644 --- a/src/main/java/thaumicenergistics/common/features/RecipeRegistration.java +++ b/src/main/java/thaumicenergistics/common/features/RecipeRegistration.java @@ -128,7 +128,7 @@ public final class RecipeRegistration { "lBl", "QPQ", 'Q', ae("quartz_glass"), - 'l', new ItemStack(Items.LAPIS_LAZULI, 4), + 'l', new ItemStack(Items.LAPIS_LAZULI, 1), 'B', tc("zombie_brain"), 'P', ae("calculation_processor")); @@ -138,7 +138,7 @@ public final class RecipeRegistration { "qWq", "FqA", 'A', tc("air_shard"), - 'q', new ItemStack(Items.QUARTZ, 4), + 'q', new ItemStack(Items.QUARTZ, 1), 'F', tc("fire_shard"), 'W', ae("certus_quartz_wrench")); @@ -156,22 +156,23 @@ public final class RecipeRegistration { ESSENTIA_IMPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_IMPORT_BUS), new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1), - "JDJ", + // 注意:TC ShapedArcaneRecipe 要求模式为矩形(各行等长),删 J 后必须补空格占位 + "JD ", "IFI", " ", - 'J', tc("warded_jar"), + 'J', tc("jar_of_essentia"), 'D', stack(ModItems.DIFFUSION_CORE), - 'I', new ItemStack(Items.IRON_INGOT, 2), + 'I', new ItemStack(Items.IRON_INGOT, 1), 'F', tc("essentia_filter")); ESSENTIA_EXPORT_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_EXPORT_BUS), new AspectList().add(Aspect.FIRE, 2).add(Aspect.EARTH, 2).add(Aspect.WATER, 1), - "JCJ", + "JC ", "IFI", " ", - 'J', tc("warded_jar"), + 'J', tc("jar_of_essentia"), 'C', stack(ModItems.COALESCENCE_CORE), - 'I', new ItemStack(Items.IRON_INGOT, 2), + 'I', new ItemStack(Items.IRON_INGOT, 1), 'F', tc("essentia_filter")); ESSENTIA_STORAGE_BUS = ThaumcraftApi.addArcaneCraftingRecipe("ESSENTIABUSES", stack(ModItems.ESSENTIA_STORAGE_BUS), @@ -182,7 +183,7 @@ public final class RecipeRegistration { 'D', stack(ModItems.DIFFUSION_CORE), 'C', stack(ModItems.COALESCENCE_CORE), 'F', tc("essentia_filter"), - 'I', new ItemStack(Items.IRON_INGOT, 2), + 'I', new ItemStack(Items.IRON_INGOT, 1), 'G', ae("quartz_glass")); KNOWLEDGE_INSCRIBER_BLOCK = ThaumcraftApi.addArcaneCraftingRecipe("KNOWLEDGEINSCRIBER", stack(ModBlocks.KNOWLEDGE_INSCRIBER), @@ -190,7 +191,7 @@ public final class RecipeRegistration { "ILI", " T ", "IPI", - 'I', new ItemStack(Items.IRON_INGOT, 4), + 'I', new ItemStack(Items.IRON_INGOT, 1), 'L', MONITORS, 'T', tc("thaumonomicon"), 'P', ae("logic_processor")); @@ -200,7 +201,7 @@ public final class RecipeRegistration { "ILI", " G ", "IPI", - 'I', new ItemStack(Items.IRON_INGOT, 4), + 'I', new ItemStack(Items.IRON_INGOT, 1), 'L', MONITORS, 'G', tc("thaumometer"), 'P', ae("engineering_processor")); @@ -252,7 +253,7 @@ public final class RecipeRegistration { // 6. 源流接口 VIS_INTERFACE = ThaumcraftApi.addShapelessArcaneCraftingRecipe("VISINTERFACE", stack(ModItems.VIS_INTERFACE), new AspectList().add(Aspect.AIR, 2).add(Aspect.EARTH, 2).add(Aspect.FIRE, 2).add(Aspect.WATER, 2).add(Aspect.ORDER, 2).add(Aspect.ENTROPY, 2), - tc("balance_shard"), + tc("balanced_shard"), ae("me_p2p_tunnel")); // 7. 原质振荡室 @@ -293,27 +294,38 @@ public final class RecipeRegistration { tc("salis_mundus")); // 3. 奥术装配器 + // 权杖要求:6 个原质要素值全满(满充能)。createFullVisScepter() 生成"满充能"预期栈, + // TC port 的 InfusionRecipe.hasExpectedComponents 会逐键比对 custom_data—— + // 预期键必须全部存在于实物且数值相等 → 未满充能的权杖无法匹配。 + // 组件数:8 个(1.7.10 原版一致)。实测 9 组件(含世界盐)注魔祭坛无法启动—— + // TC 自身所有注魔配方组件数上限就是 8。 ARCANE_ASSEMBLER = ThaumcraftApi.addInfusionCraftingRecipe("ARCANEASSEMBLER", stack(ModBlocks.ARCANE_ASSEMBLER), 5, new AspectList().add(Aspect.CRAFT, 64).add(Aspect.EXCHANGE, 32).add(Aspect.AURA, 16).add(Aspect.MAGIC, 16).add(Aspect.METAL, 8).add(Aspect.CRYSTAL, 8), ae("molecular_assembler"), tc("fire_shard"), tc("entropy_shard"), tc("order_shard"), - tc("balance_shard"), + tc("balanced_shard"), tc("water_shard"), tc("earth_shard"), tc("air_shard"), - tc("salis_mundus"), createFullVisScepter()); - - - } + /** + * 生成"满充能"银木神秘权杖作为注魔配方的预期组件。 + * + * ⚠ 关键:不要调用 TCVisHelper.setWandParts()!玩家实际合成的权杖(ArcaneSceptreRecipe → + * WandCastingItem.createWandFor)是纯物品,custom_data 只有充能时写入的 tc_vis_* 键, + * 没有 tc_wand_rod/tc_wand_cap。若在预期侧写入这些键,hasExpectedComponents 逐键比对时 + * 会因实物缺少而永不匹配——满充能权杖也放不进去。 + * + * getWandMaxVis 用 fallbackVariant "silverwood_thaumium_scepter" 解析即可得 15000 + * (银木杆 10000 × 权杖 1.5),无需依赖栈上的部件数据。 + */ private static ItemStack createFullVisScepter() { ItemStack scepter = tc("silverwood_thaumium_scepter"); if (scepter.isEmpty()) return scepter; - TCVisHelper.setWandParts(scepter, "silverwood", "thaumium", true); int max = TCVisHelper.getWandMaxVis(scepter, "silverwood_thaumium_scepter"); for (Aspect a : Aspect.getPrimalAspects()) { TCVisHelper.storeVis(scepter, a, max, max); diff --git a/src/main/java/thaumicenergistics/common/grid/EssentiaGridService.java b/src/main/java/thaumicenergistics/common/grid/EssentiaGridService.java index 60ca826..8a03b7d 100644 --- a/src/main/java/thaumicenergistics/common/grid/EssentiaGridService.java +++ b/src/main/java/thaumicenergistics/common/grid/EssentiaGridService.java @@ -7,13 +7,13 @@ import java.util.Map; import net.minecraft.resources.ResourceLocation; +import appeng.api.config.Actionable; import appeng.api.networking.IGrid; import appeng.api.networking.IGridServiceProvider; import appeng.api.networking.security.IActionSource; import appeng.api.networking.storage.IStorageService; import appeng.api.storage.MEStorage; import appeng.api.storage.StorageHelper; -import appeng.api.networking.security.IActionSource; import thaumicenergistics.api.grid.IEssentiaGrid; import thaumicenergistics.api.storage.AspectStack; import thaumicenergistics.api.storage.IAspectStack; @@ -101,11 +101,28 @@ public class EssentiaGridService implements IEssentiaGrid, IGridServiceProvider @Override public long injectEssentia(ResourceLocation aspectId, long amount, boolean simulate) { if (amount <= 0) return 0; - if (!simulate) { - long current = monitor.get(aspectId); - monitor.set(aspectId, current + amount); + // 写入 AE2 原生存储(源质存储元件/存储总线可见、持久化) + var nativeInjected = injectNative(aspectId, amount, simulate); + if (nativeInjected < amount) { + // 原生存储空间不足时剩余部分退回内存 monitor(供注魔供应器等内部读取) + long remaining = amount - nativeInjected; + if (!simulate) { + long current = monitor.get(aspectId); + monitor.set(aspectId, current + remaining); + } + return amount - nativeInjected - remaining; // 全部接受(monitor 兜底) } - return 0; + 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 diff --git a/src/main/java/thaumicenergistics/common/integration/jade/VibrationChamberProvider.java b/src/main/java/thaumicenergistics/common/integration/jade/VibrationChamberProvider.java index 61636f3..5997cc5 100644 --- a/src/main/java/thaumicenergistics/common/integration/jade/VibrationChamberProvider.java +++ b/src/main/java/thaumicenergistics/common/integration/jade/VibrationChamberProvider.java @@ -34,11 +34,11 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer CompoundTag data = accessor.getServerData(); if (data == null || data.isEmpty()) return; - // 吸力类型 + // 当前仓内源质类型(通配吸力模式下显示仓内实际燃料) if (data.contains(TAG_ASPECT)) { String aspect = data.getString(TAG_ASPECT); String aspectKey = "thaumicenergistics.aspect." + aspect; - tooltip.add(Component.translatable("thaumicenergistics.jade.suction_type", + tooltip.add(Component.translatable("thaumicenergistics.jade.current_aspect", Component.translatable(aspectKey))); } @@ -93,8 +93,8 @@ public enum VibrationChamberProvider implements IBlockComponentProvider, IServer data.putDouble(TAG_ENERGY_MAX, chamber.getMaxEnergyStorage()); data.putDouble(TAG_ENERGY_OUTPUT, chamber.getMaxOutputPerTick()); data.putBoolean(TAG_GENERATING, chamber.isActive()); - if (chamber.getSuctionType(null) != null) { - data.putString(TAG_ASPECT, chamber.getSuctionType(null).getTag()); + if (chamber.getEssentiaType(null) != null) { + data.putString(TAG_ASPECT, chamber.getEssentiaType(null).getTag()); } } } diff --git a/src/main/java/thaumicenergistics/common/items/WirelessEssentiaTerminalItem.java b/src/main/java/thaumicenergistics/common/items/WirelessEssentiaTerminalItem.java index ca785b0..9b21f93 100644 --- a/src/main/java/thaumicenergistics/common/items/WirelessEssentiaTerminalItem.java +++ b/src/main/java/thaumicenergistics/common/items/WirelessEssentiaTerminalItem.java @@ -11,7 +11,6 @@ import net.minecraft.world.phys.BlockHitResult; import appeng.helpers.WirelessTerminalMenuHost; import appeng.items.tools.powered.WirelessTerminalItem; import appeng.menu.locator.ItemMenuHostLocator; -import appeng.menu.me.common.MEStorageMenu; /** * 无线源质终端物品。继承 AE2 的WirelessTerminalItem @@ -25,7 +24,7 @@ public class WirelessEssentiaTerminalItem extends WirelessTerminalItem { @Override public MenuType getMenuType() { - return MEStorageMenu.WIRELESS_TYPE; + return thaumicenergistics.init.ModMenuTypes.WIRELESS_ESSENTIA_TERMINAL.get(); } @Override diff --git a/src/main/java/thaumicenergistics/common/network/ThENetworking.java b/src/main/java/thaumicenergistics/common/network/ThENetworking.java index 346a1ea..22e17c7 100644 --- a/src/main/java/thaumicenergistics/common/network/ThENetworking.java +++ b/src/main/java/thaumicenergistics/common/network/ThENetworking.java @@ -50,6 +50,20 @@ public final class ThENetworking { (pkt, ctx) -> pkt.handleOnClient(ctx) ); + // EssentiaTerminalFillC2SPacket — C2S(客户端→服务端,源质终端右键装罐) + registrar.playToServer( + EssentiaTerminalFillC2SPacket.TYPE, + EssentiaTerminalFillC2SPacket.STREAM_CODEC, + (pkt, ctx) -> pkt.handleOnServer(ctx) + ); + + // EssentiaTerminalDepositC2SPacket — C2S(客户端→服务端,手持源质容器右键存入网络) + registrar.playToServer( + EssentiaTerminalDepositC2SPacket.TYPE, + EssentiaTerminalDepositC2SPacket.STREAM_CODEC, + (pkt, ctx) -> pkt.handleOnServer(ctx) + ); + // ResearchGateErrorPacket — S2C(服务端→客户端,研究未解锁拦截提示,携带研究键) registrar.playToClient( ResearchGateErrorPacket.TYPE, diff --git a/src/main/java/thaumicenergistics/common/parts/EssentiaExportBusPart.java b/src/main/java/thaumicenergistics/common/parts/EssentiaExportBusPart.java index 0692822..ae4573c 100644 --- a/src/main/java/thaumicenergistics/common/parts/EssentiaExportBusPart.java +++ b/src/main/java/thaumicenergistics/common/parts/EssentiaExportBusPart.java @@ -60,7 +60,6 @@ public class EssentiaExportBusPart extends IOBusPart private static final double IDLE_POWER = 0.5; private static final double TRANSFER_POWER = EssentiaConversionHelper.AE_PER_ESSENTIA; - private static final int TRANSFER_RATE = 8; private boolean isWorkingFlag = false; @@ -70,6 +69,14 @@ public class EssentiaExportBusPart extends IOBusPart private long storedEnergy = 0; + /** + * 单次导出量 = 基础速率 8 × 每 tick 操作次数(受加速卡影响)。 + * AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。 + */ + private long getExportRate() { + return 8L * Math.max(1, getOperationsPerTick()); + } + public EssentiaExportBusPart(IPartItem partItem) { super(TickRates.ExportBus, Set.of(AEssentiaKeyType.INSTANCE), partItem); this.getMainNode().setIdlePowerUsage(IDLE_POWER); @@ -122,7 +129,7 @@ public class EssentiaExportBusPart extends IOBusPart if (key == null) return false; long extracted = grid.getStorageService().getInventory().extract( - key, TRANSFER_RATE, Actionable.MODULATE, IActionSource.ofMachine(this)); + key, getExportRate(), Actionable.MODULATE, IActionSource.ofMachine(this)); if (extracted <= 0) return false; // 尝试注入到相邻方块:先 IAspectContainer,再回退到 IEssentiaTransport diff --git a/src/main/java/thaumicenergistics/common/parts/EssentiaImportBusPart.java b/src/main/java/thaumicenergistics/common/parts/EssentiaImportBusPart.java index 6922281..71606d1 100644 --- a/src/main/java/thaumicenergistics/common/parts/EssentiaImportBusPart.java +++ b/src/main/java/thaumicenergistics/common/parts/EssentiaImportBusPart.java @@ -62,6 +62,14 @@ public class EssentiaImportBusPart extends IOBusPart private boolean isWorkingFlag = false; + /** + * 单次传输量 = 基础速率 × 每 tick 操作次数(受加速卡影响)。 + * AE2 加速卡语义:0卡=1、1卡=8、2卡=32、3卡=64、4卡=96(getOperationsPerTick)。 + */ + private int getTransferRate() { + return TRANSFER_RATE * Math.max(1, getOperationsPerTick()); + } + private final KeyTypeSelection keyTypeSelection = new KeyTypeSelection( selection -> { }, keyType -> keyType == AEssentiaKeyType.INSTANCE); @@ -104,7 +112,7 @@ public class EssentiaImportBusPart extends IOBusPart BlockEntity adjacentBE = level.getBlockEntity(adjacentPos); if (adjacentBE == null) return false; - AspectStack extracted = TCReflection.extractEssentia(adjacentBE, TRANSFER_RATE); + AspectStack extracted = TCReflection.extractEssentia(adjacentBE, getTransferRate()); if (extracted == null || extracted.isEmpty()) return false; long injected = this.injectToNetwork(grid, extracted); diff --git a/src/main/java/thaumicenergistics/common/parts/EssentiaTerminalPart.java b/src/main/java/thaumicenergistics/common/parts/EssentiaTerminalPart.java index 57f1e1c..0f340ac 100644 --- a/src/main/java/thaumicenergistics/common/parts/EssentiaTerminalPart.java +++ b/src/main/java/thaumicenergistics/common/parts/EssentiaTerminalPart.java @@ -10,7 +10,6 @@ import appeng.api.parts.IPartItem; import appeng.api.parts.IPartModel; import appeng.api.util.KeyTypeSelection; import appeng.items.parts.PartModels; -import appeng.menu.me.common.MEStorageMenu; import appeng.parts.PartModel; import appeng.parts.reporting.AbstractTerminalPart; import thaumicenergistics.ThaumicEnergistics; @@ -54,7 +53,7 @@ public class EssentiaTerminalPart extends AbstractTerminalPart { @Override public MenuType getMenuType(Player player) { - return MEStorageMenu.TYPE; + return thaumicenergistics.init.ModMenuTypes.ESSENTIA_TERMINAL.get(); } @Override diff --git a/src/main/java/thaumicenergistics/common/parts/VisInterfacePart.java b/src/main/java/thaumicenergistics/common/parts/VisInterfacePart.java index fb3b0e2..9d15c88 100644 --- a/src/main/java/thaumicenergistics/common/parts/VisInterfacePart.java +++ b/src/main/java/thaumicenergistics/common/parts/VisInterfacePart.java @@ -53,6 +53,9 @@ public class VisInterfacePart extends P2PTunnelPart implements /** 抽取颜色在没有新抽取后保持点亮的时间(ms),超时清除。 */ private static final long TIME_TO_CLEAR = 500L; + /** 懒 tick 周期:固定 30 tick(vis 传输为低频操作,无需实时轮询)。 */ + private static final int TICK_RATE = 30; + private static final String NBT_KEY_UID = "uid"; private static final String NBT_KEY_IS_PROVIDER = "isProvider"; @@ -290,7 +293,7 @@ public class VisInterfacePart extends P2PTunnelPart implements @Override public TickingRequest getTickingRequest(IGridNode node) { - return new TickingRequest(30, 30, false); + return new TickingRequest(TICK_RATE, TICK_RATE, false); } @Override diff --git a/src/main/java/thaumicenergistics/common/tiles/TileArcaneAssembler.java b/src/main/java/thaumicenergistics/common/tiles/TileArcaneAssembler.java index e46d2f4..a5a8416 100644 --- a/src/main/java/thaumicenergistics/common/tiles/TileArcaneAssembler.java +++ b/src/main/java/thaumicenergistics/common/tiles/TileArcaneAssembler.java @@ -51,7 +51,6 @@ import thaumcraft.api.aspects.AspectList; import thaumcraft.api.crafting.IArcaneRecipe; import thaumicenergistics.api.grid.IEssentiaGrid; -import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.common.integration.tc.ArcaneCraftingPattern; import thaumicenergistics.common.integration.tc.TCReflection; import thaumicenergistics.common.inventory.HandlerKnowledgeCore; @@ -314,7 +313,6 @@ public class TileArcaneAssembler extends ThETileBase boolean enoughVis = hasEnoughVisForCraft(); if (!enoughVis) { - ThaumicEnergistics.LOG.debug("[AA] craftingTick: vis 不足,等待补充 (storedVis cvis={})", storedVis.visSize()); return TickRateModulation.SAME; } @@ -331,7 +329,6 @@ public class TileArcaneAssembler extends ThETileBase long inserted = storage.getInventory().insert( outputKey, output.getCount(), Actionable.SIMULATE, actionSource); if (inserted < output.getCount()) { - ThaumicEnergistics.LOG.debug("[AA] craftingTick: 产物无法插入网络(模拟 inserted={}/{}),等待", inserted, output.getCount()); return TickRateModulation.SAME; } @@ -339,7 +336,6 @@ public class TileArcaneAssembler extends ThETileBase storage.getInventory().insert( outputKey, output.getCount(), Actionable.MODULATE, actionSource); - ThaumicEnergistics.LOG.debug("[AA] craftingTick: 完成!插入产物 {}x{} 到网络", outputKey, output.getCount()); isCrafting = false; currentRecipe = null; @@ -359,11 +355,8 @@ public class TileArcaneAssembler extends ThETileBase double extracted = energy.extractAEPower(need, Actionable.MODULATE, PowerMultiplier.CONFIG); if (extracted >= need * 0.9) { craftTickCounter += ticksSinceLast; - ThaumicEnergistics.LOG.debug("[AA] craftingTick: 推进 {}/{} (抽电 {}/{})", craftTickCounter, ticksPerCraft, extracted, need); // 节流同步合成进度到客户端(GUI 第7格进度条动画) maybeMarkVisDirty(); - } else { - ThaumicEnergistics.LOG.debug("[AA] craftingTick: 能量不足 (抽到 {}/{}),不推进", extracted, need); } } @@ -446,19 +439,13 @@ public class TileArcaneAssembler extends ThETileBase ArcaneAssemblerPattern ap = buildArcanePattern(acp); if (ap != null) { result.add(ap); - ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: pattern primaryOutput={}, definition={}", - ap.getPrimaryOutput(), ap.getDefinition()); } } - ThaumicEnergistics.LOG.debug("[AA] getAvailablePatterns: kCore valid={}, handler patterns={}, returned={}", - !kCore.isEmpty(), patterns.size(), result.size()); return result; } @Override public boolean pushPattern(IPatternDetails patternDetails, KeyCounter[] inputHolder) { - ThaumicEnergistics.LOG.info("[AA] pushPattern(provider) called, pattern={}, isArcanePattern={}", - patternDetails, patternDetails instanceof ArcaneAssemblerPattern); if (patternDetails instanceof ArcaneAssemblerPattern arcanePattern) { return beginCrafting(arcanePattern); } @@ -490,9 +477,6 @@ public class TileArcaneAssembler extends ThETileBase private boolean beginCrafting(ArcaneAssemblerPattern arcanePattern) { AspectList aspects = arcanePattern.getAspects(); ItemStack result = arcanePattern.getResultStack(); - ThaumicEnergistics.LOG.info("[AA] beginCrafting: isCrafting={}, active={}, aspectsEmpty={}, resultEmpty={}", - isCrafting, mainNode.isActive(), - aspects == null || aspects.isEmpty(), result.isEmpty()); if (isCrafting || !mainNode.isActive()) return false; if (aspects == null || aspects.isEmpty() || result.isEmpty()) return false; @@ -504,7 +488,6 @@ public class TileArcaneAssembler extends ThETileBase this.internalInventory.setItem(TARGET_SLOT, result.copy()); setChanged(); markForUpdate(); - ThaumicEnergistics.LOG.info("[AA] beginCrafting: STARTED, result={}, aspects={}", result, aspects.visSize()); return true; } @@ -593,7 +576,6 @@ public class TileArcaneAssembler extends ThETileBase // 守卫 getGrid()!=null:装配器未连 ME 网络(无相邻 cable)时 getGrid() 为 null, // 此时 requestUpdate 内部 node.getGrid().getCraftingService() 会 NPE,导致 kCore 插入事务回滚 → "无法绑定"。 if (level != null && !level.isClientSide() && mainNode.getGrid() != null) { - ThaumicEnergistics.LOG.info("[AA] kCore slot changed → requestUpdate"); ICraftingProvider.requestUpdate(mainNode); } } diff --git a/src/main/java/thaumicenergistics/common/tiles/TileEssentiaVibrationChamber.java b/src/main/java/thaumicenergistics/common/tiles/TileEssentiaVibrationChamber.java index c5997e0..3d7875c 100644 --- a/src/main/java/thaumicenergistics/common/tiles/TileEssentiaVibrationChamber.java +++ b/src/main/java/thaumicenergistics/common/tiles/TileEssentiaVibrationChamber.java @@ -40,12 +40,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl private static final int MAX_ESSENTIA = 64; private static final int BASE_BURN_TICKS = 800; - private static final double BASE_AE_PER_TICK = 20.0; + private static final double BASE_AE_PER_TICK = 200.0; private static final int IDLE_POWER = 1; - // 能量槽配置:8KFE = 8000 AE,对外最大输出 400 AE/t - public static final double MAX_ENERGY_STORAGE = 8000.0; - public static final double MAX_OUTPUT_PER_TICK = 400.0; + // 能量槽配置:16KFE = 16000 AE,对外最大输出 1000 AE/t + public static final double MAX_ENERGY_STORAGE = 16000.0; + public static final double MAX_OUTPUT_PER_TICK = 2000.0; + + // 懒 tick 周期:燃烧中 10 tick、空闲 40 tick(能量生产无需实时响应,低频即可) + private static final int TICK_RATE_BURNING = 10; + private static final int TICK_RATE_IDLE = 40; protected final IManagedGridNode mainNode; private final IGridNodeListener nodeListener = new IGridNodeListener<>() { @@ -62,7 +66,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl private double aePerTick; private int storedEssentia; private Aspect currentAspect; - private Aspect suctionAspect = Aspect.ENERGY; private double storedEnergy; @@ -97,7 +100,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl @Override public TickingRequest getTickingRequest(IGridNode node) { - return new TickingRequest(10, 40, false); + return new TickingRequest(TICK_RATE_BURNING, TICK_RATE_IDLE, false); } @Override @@ -177,8 +180,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl int srcAmt = source.getEssentiaAmount(sourceSide); if (!canOut) continue; if (srcType == null) continue; - String srcTag = srcType.getTag(); - if (!srcTag.equals("ignis") && !srcTag.equals("potentia") && !srcTag.equals("energy")) continue; + // 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2) if (space <= 0) return; @@ -187,7 +189,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl if (taken > 0) { storedEssentia += taken; currentAspect = srcType; - suctionAspect = srcType; + // suctionAspect 固定为 null(通配吸力),不锁定单一类型 setChanged(); return; } @@ -240,9 +242,25 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl private boolean startBurning() { if (currentAspect == null) currentAspect = Aspect.FIRE; - boolean isPotentia = currentAspect != null && (currentAspect == Aspect.ENERGY || currentAspect.getTag().equals("potentia") || currentAspect.getTag().equals("energy")); - int burnTime = isPotentia ? (int)(BASE_BURN_TICKS / 1.6f) : BASE_BURN_TICKS / 2; - double power = isPotentia ? BASE_AE_PER_TICK * 1.6 : BASE_AE_PER_TICK; + String tag = currentAspect.getTag(); + boolean isPotentia = currentAspect == Aspect.ENERGY || tag.equals("potentia") || tag.equals("energy"); + boolean isOther = !tag.equals("ignis") && !isPotentia; + + int burnTime; + double power; + if (isPotentia) { + // 能量:1.6 倍功率,燃烧更久 + burnTime = (int)(BASE_BURN_TICKS / 1.6f); + power = BASE_AE_PER_TICK * 1.6; + } else if (isOther) { + // 其他所有要素:基础效率的 1/2 + burnTime = BASE_BURN_TICKS / 2; + power = BASE_AE_PER_TICK / 2.0; + } else { + // 火:基础效率 + burnTime = BASE_BURN_TICKS / 2; + power = BASE_AE_PER_TICK; + } burnTicksRemaining = burnTime; totalBurnTicks = burnTime; @@ -254,17 +272,16 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl @Override public boolean canInputFrom(Direction side) { return true; } @Override public boolean canOutputTo(Direction side) { return true; } @Override public void setSuction(Aspect aspect, int amount) { - this.suctionAspect = aspect; + // 通配吸力:始终吸所有源质类型,忽略外部设置 } @Override public Aspect getSuctionType(Direction side) { - return suctionAspect; + return null; // null = 通配吸力:管道可输送任意类型源质 } @Override public int getSuctionAmount(Direction side) { return 128; } private boolean isFuelAspect(Aspect aspect) { - if (aspect == null) return false; - String tag = aspect.getTag(); - return tag.equals("ignis") || tag.equals("potentia") || tag.equals("energy"); + // 所有要素均可作为燃料(火/能量效率更高,其他要素 1/2) + return aspect != null; } @Override @@ -298,11 +315,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl @Override public boolean renderExtendedTube() { return false; } public void setSuctionType(Aspect aspect) { - if (aspect == Aspect.FIRE || aspect == Aspect.ENERGY) { - suctionAspect = aspect; - setChanged(); - markForUpdate(); - } + // 通配吸力模式下无需设置类型;保留方法避免编译错误 } public int getStoredEssentia() { return storedEssentia; } @@ -338,7 +351,7 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl storedEnergy = tag.getDouble("StoredEnergy"); if (storedEnergy > MAX_ENERGY_STORAGE) storedEnergy = MAX_ENERGY_STORAGE; if (tag.contains("AspectTag")) currentAspect = Aspect.getAspect(tag.getString("AspectTag")); - if (tag.contains("SuctionTag")) suctionAspect = Aspect.getAspect(tag.getString("SuctionTag")); + // suctionAspect 固定为 null(通配吸力),不读取 SuctionTag } @Override @@ -352,6 +365,6 @@ public class TileEssentiaVibrationChamber extends ThETileBase implements IInWorl tag.putInt("StoredEssentia", storedEssentia); tag.putDouble("StoredEnergy", storedEnergy); if (currentAspect != null) tag.putString("AspectTag", currentAspect.getTag()); - if (suctionAspect != null) tag.putString("SuctionTag", suctionAspect.getTag()); + // suctionAspect 固定为 null(通配吸力),不写入 SuctionTag } } \ No newline at end of file diff --git a/src/main/java/thaumicenergistics/init/ModMenuTypes.java b/src/main/java/thaumicenergistics/init/ModMenuTypes.java index 7f12ac1..bfdd7da 100644 --- a/src/main/java/thaumicenergistics/init/ModMenuTypes.java +++ b/src/main/java/thaumicenergistics/init/ModMenuTypes.java @@ -1,5 +1,6 @@ package thaumicenergistics.init; +import appeng.api.implementations.menuobjects.IPortableTerminal; import appeng.api.storage.ITerminalHost; import appeng.menu.implementations.MenuTypeBuilder; import net.minecraft.core.registries.Registries; @@ -27,8 +28,16 @@ public final class ModMenuTypes { REGISTRY.register("essentia_vibration_chamber", () -> IMenuTypeExtension.create(ContainerEssentiaVibrationChamber::new)); public static final Supplier> ARCANE_ASSEMBLER = REGISTRY.register("arcane_assembler", () -> IMenuTypeExtension.create(ContainerArcaneAssembler::new)); - public static final Supplier> ESSENTIA_TERMINAL = - REGISTRY.register("essentia_terminal", () -> IMenuTypeExtension.create(ContainerEssentiaCellTerminal::new)); + /** 源质终端菜单:有线段(挂在线缆上的终端部件)。 */ + public static final Supplier> ESSENTIA_TERMINAL = + REGISTRY.register("essentia_terminal", () -> + MenuTypeBuilder.create(EssentiaTerminalMenu::new, ITerminalHost.class) + .buildUnregistered(ThaumicEnergistics.id("essentia_terminal"))); + /** 源质终端菜单:无线段(手持无线源质终端)。 */ + public static final Supplier> WIRELESS_ESSENTIA_TERMINAL = + REGISTRY.register("wireless_essentia_terminal", () -> + MenuTypeBuilder.create(EssentiaTerminalMenu::new, IPortableTerminal.class) + .buildUnregistered(ThaumicEnergistics.id("wireless_essentia_terminal"))); public static final Supplier> KNOWLEDGE_INSCRIBER = REGISTRY.register("knowledge_inscriber", () -> IMenuTypeExtension.create(ContainerKnowledgeInscriber::new)); public static final Supplier> DISTILLATION_ENCODER = @@ -38,28 +47,28 @@ public final class ModMenuTypes { REGISTRY.register("essentia_import_bus", () -> MenuTypeBuilder.create(ContainerEssentiaImportBus::new, EssentiaImportBusPart.class) .withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty()) - .build("essentia_import_bus")); + .buildUnregistered(ThaumicEnergistics.id("essentia_import_bus"))); public static final Supplier> ESSENTIA_EXPORT_BUS = REGISTRY.register("essentia_export_bus", () -> MenuTypeBuilder.create(ContainerEssentiaExportBus::new, EssentiaExportBusPart.class) .withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty()) - .build("essentia_export_bus")); + .buildUnregistered(ThaumicEnergistics.id("essentia_export_bus"))); public static final Supplier> ESSENTIA_STORAGE_BUS = REGISTRY.register("essentia_storage_bus", () -> MenuTypeBuilder.create(ContainerEssentiaStorageBus::new, EssentiaStorageBusPart.class) .withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty()) - .build("essentia_storage_bus")); + .buildUnregistered(ThaumicEnergistics.id("essentia_storage_bus"))); public static final Supplier> ARCANE_CRAFTING_TERMINAL = REGISTRY.register("arcane_crafting_terminal", () -> MenuTypeBuilder.create(ContainerArcaneCraftingTerminal::new, ITerminalHost.class) - .build("arcane_crafting_terminal")); + .buildUnregistered(ThaumicEnergistics.id("arcane_crafting_terminal"))); public static final Supplier> ESSENTIA_LEVEL_EMITTER = REGISTRY.register("essentia_level_emitter", () -> MenuTypeBuilder.create(ContainerEssentiaLevelEmitter::new, EssentiaLevelEmitterPart.class) .withMenuTitle(host -> host.getName() != null ? host.getName() : Component.empty()) - .build("essentia_level_emitter")); + .buildUnregistered(ThaumicEnergistics.id("essentia_level_emitter"))); } diff --git a/src/main/java/thaumicenergistics/integration/jei/ThEJeiPlugin.java b/src/main/java/thaumicenergistics/integration/jei/ThEJeiPlugin.java index ccb549e..6d7647c 100644 --- a/src/main/java/thaumicenergistics/integration/jei/ThEJeiPlugin.java +++ b/src/main/java/thaumicenergistics/integration/jei/ThEJeiPlugin.java @@ -49,9 +49,8 @@ public class ThEJeiPlugin implements IModPlugin { Class.forName("tamaized.ae2jeiintegration.api.integrations.jei.IngredientConverters"); Class.forName("thaumicenergistics.integration.jei.Ae2JeiCompat") .getMethod("register").invoke(null); - ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration detected, essentia drag-conversion registered"); } catch (ClassNotFoundException e) { - ThaumicEnergistics.LOG.info("[JEI] AE2-JEI-Integration not present, essentia drag-conversion unavailable (optional)"); + // AE2-JEI-Integration 未安装:跳过源质拖拽转换(可选功能,不影响其他 JEI 功能) } catch (ReflectiveOperationException e) { ThaumicEnergistics.LOG.warn("[JEI] AE2-JEI-Integration registration failed: {}", e.toString()); } @@ -83,7 +82,6 @@ public class ThEJeiPlugin implements IModPlugin { @Override public void registerRecipeTransferHandlers(IRecipeTransferRegistration registration) { - ThaumicEnergistics.LOG.info("[JEI] Registering ThEUseCraftingRecipeTransfer for ArcaneCraftingTerminal"); // 注册 IRecipeTransferInfo 让 JEI 显示 "+" 按钮 registration.addRecipeTransferHandler(new ArcaneCraftingTransferInfo()); registration.addRecipeTransferHandler(new ArcaneRecipeTransferInfo()); diff --git a/src/main/java/thaumicenergistics/integration/jei/ThEUseArcaneRecipeTransfer.java b/src/main/java/thaumicenergistics/integration/jei/ThEUseArcaneRecipeTransfer.java index 4ff0318..9450a27 100644 --- a/src/main/java/thaumicenergistics/integration/jei/ThEUseArcaneRecipeTransfer.java +++ b/src/main/java/thaumicenergistics/integration/jei/ThEUseArcaneRecipeTransfer.java @@ -1,5 +1,7 @@ package thaumicenergistics.integration.jei; +import java.util.HashMap; +import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -24,10 +26,14 @@ import net.neoforged.neoforge.network.PacketDistributor; import thaumcraft.integration.jei.JeiArcaneRecipe; import thaumcraft.integration.jei.ThaumcraftJeiPlugin; -import thaumicenergistics.ThaumicEnergistics; import thaumicenergistics.common.container.ContainerArcaneCraftingTerminal; import thaumicenergistics.init.ModMenuTypes; +import static appeng.integration.modules.itemlists.TransferHelper.BLUE_PLUS_BUTTON_COLOR; +import static appeng.integration.modules.itemlists.TransferHelper.BLUE_SLOT_HIGHLIGHT_COLOR; +import static appeng.integration.modules.itemlists.TransferHelper.ORANGE_PLUS_BUTTON_COLOR; +import static appeng.integration.modules.itemlists.TransferHelper.RED_SLOT_HIGHLIGHT_COLOR; + /** * 奥术合成终端专用的 JEI 奥术配方转移处理器。 * 点击 "+" 时发送 FillCraftingGridFromRecipePacket 从 ME 网络填充材料。 @@ -65,13 +71,31 @@ public class ThEUseArcaneRecipeTransfer boolean maxTransfer, boolean doTransfer) { - ThaumicEnergistics.LOG.info("[JEI-Arcane] transferRecipe called, recipeId={}, doTransfer={}", - recipe.id(), doTransfer); - boolean craftMissing = AbstractContainerScreen.hasControlDown(); - // 预览:始终 null(绿色按钮),由服务端处理实际缺失 + // 构建 3x3 合成格 slot→ingredient 映射,检测材料缺失(与普通配方一致) + var slotToIngredientMap = buildSlotToIngredientMap(recipe); + var missingSlots = menu.findMissingIngredients(slotToIngredientMap); + + // 全部缺失 → 红色,无法点击 + if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) { + var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT); + var missingSlotViews = missingSlots.missingSlots().stream() + .map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null) + .filter(Objects::nonNull) + .toList(); + return helper.createUserErrorForMissingSlots( + appeng.core.localization.ItemModText.NO_ITEMS.text(), missingSlotViews); + } + + // 预览:检查缺失情况,返回对应的错误渲染 if (!doTransfer) { + if (missingSlots.totalSize() != 0) { + int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR; + // 有缺料 → Type.ERROR 阻止转移(按钮不可点),有料也不允许部分填充 + return new ThEUseCraftingRecipeTransfer.MissingSlotsError( + missingSlots, craftMissing, color, missingSlots.anyMissing(), menu); + } return null; } @@ -88,10 +112,23 @@ public class ThEUseArcaneRecipeTransfer // 奥术配方不在 vanilla 配方管理器中,recipeId 传 null // 服务端会用 Ingredient.of(templateItems) 重建,然后从 ME 网络提取 var packet = new FillCraftingGridFromRecipePacket(null, templateItems, craftMissing); - ThaumicEnergistics.LOG.info("[JEI-Arcane] Sending FillCraftingGridFromRecipePacket, templateCount={}", - templateItems.stream().filter(s -> !s.isEmpty()).count()); PacketDistributor.sendToServer(packet); return null; } + + /** + * 根据奥术配方 inputs(9 槽 List>)构建 3x3 合成格 slot → ingredient 映射。 + */ + private static Map buildSlotToIngredientMap(JeiArcaneRecipe recipe) { + Map 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; + } } diff --git a/src/main/java/thaumicenergistics/integration/jei/ThEUseCraftingRecipeTransfer.java b/src/main/java/thaumicenergistics/integration/jei/ThEUseCraftingRecipeTransfer.java index 1554211..55d0be8 100644 --- a/src/main/java/thaumicenergistics/integration/jei/ThEUseCraftingRecipeTransfer.java +++ b/src/main/java/thaumicenergistics/integration/jei/ThEUseCraftingRecipeTransfer.java @@ -82,8 +82,6 @@ public class ThEUseCraftingRecipeTransfer boolean doTransfer) { var recipe = recipeHolder.value(); - ThaumicEnergistics.LOG.info("[JEI] transferRecipe called, recipeId={}, doTransfer={}, player={}", - recipeHolder.id(), doTransfer, player.getName().getString()); if (recipe.getType() != RecipeType.CRAFTING) { ThaumicEnergistics.LOG.warn("[JEI] Not a crafting recipe, type={}", recipe.getType()); return helper.createInternalError(); @@ -96,16 +94,14 @@ public class ThEUseCraftingRecipeTransfer } boolean craftMissing = AbstractContainerScreen.hasControlDown(); - // 自行构建 slot→ingredient 映射(0-8 对应 3x3 合成格), - // 不依赖 helper.getGuiSlotIndexToIngredientMap(需要 IRecipeTransferInfo 才能正确工作) - var slotToIngredientMap = buildSlotToIngredientMap(recipe); + // 用 JEI 官方映射 helper.getGuiSlotIndexToIngredientMap:配方居中摆入 3x3 网格, + // 键空间与 JEI 输入槽视图完全一致。之前用 ensure3by3CraftingMatrix(左上对齐填充), + // 对非 3x3 配方(如 2x2 笔与墨)索引错位,导致缺失高亮画到错误的格子上。 + var slotToIngredientMap = helper.getGuiSlotIndexToIngredientMap(recipeHolder); var missingSlots = menu.findMissingIngredients(slotToIngredientMap); - ThaumicEnergistics.LOG.info("[JEI] missingSlots: missing={}, craftable={}, total={}", - missingSlots.missingSlots().size(), missingSlots.craftableSlots().size(), missingSlots.totalSize()); // 全部缺失 → 红色,无法点击 if (missingSlots.missingSlots().size() == slotToIngredientMap.size()) { - ThaumicEnergistics.LOG.info("[JEI] All items missing, returning NO_ITEMS error"); var inputSlots = display.getSlotViews(RecipeIngredientRole.INPUT); var missingSlotViews = missingSlots.missingSlots().stream() .map(idx -> idx < inputSlots.size() ? inputSlots.get(idx) : null) @@ -119,7 +115,8 @@ public class ThEUseCraftingRecipeTransfer if (!doTransfer) { if (missingSlots.totalSize() != 0) { int color = missingSlots.anyMissing() ? ORANGE_PLUS_BUTTON_COLOR : BLUE_PLUS_BUTTON_COLOR; - return new MissingSlotsError(missingSlots, craftMissing, color); + // 有缺料 → USER_FACING 阻止转移(按钮不可点);仅可合成(craftable)不阻止(可触发自动合成) + return new MissingSlotsError(missingSlots, craftMissing, color, missingSlots.anyMissing(), menu); } // 全部物品都有 → null = 绿色按钮 return null; @@ -145,42 +142,28 @@ public class ThEUseCraftingRecipeTransfer } var packet = new FillCraftingGridFromRecipePacket(recipeId, templateItems, craftMissing); - ThaumicEnergistics.LOG.info("[JEI] Sending FillCraftingGridFromRecipePacket, recipeId={}, templateCount={}, craftMissing={}", - recipeId, templateItems.stream().filter(s -> !s.isEmpty()).count(), craftMissing); PacketDistributor.sendToServer(packet); return null; } - /** - * 根据配方构建 3x3 合成格的 slot index → ingredient 映射。 - * 使用 CraftingRecipeUtil.ensure3by3CraftingMatrix 确保 9 格对齐, - * 与实际转移时的索引体系完全一致。 - */ - private static Map buildSlotToIngredientMap(CraftingRecipe recipe) { - var ingredients = CraftingRecipeUtil.ensure3by3CraftingMatrix(recipe); - Map map = new HashMap<>(); - for (int i = 0; i < ingredients.size() && i < 9; i++) { - var ing = ingredients.get(i); - if (!ing.isEmpty()) { - map.put(i, ing); - } - } - return map; - } - /** * 缺失/可合成物品的预览错误渲染器。 * 参考 AE2-JEI-Integration.UseCraftingRecipeTransfer.ErrorRenderer。 + * 供奥术配方转移(ThEUseArcaneRecipeTransfer)复用。 + * blockTransfer=true 时返回 ERROR(阻止转移,按钮灰/橙不可点)。 */ - private record MissingSlotsError( + public record MissingSlotsError( ContainerArcaneCraftingTerminal.MissingIngredientSlots indices, boolean craftMissing, - int color) implements IRecipeTransferError { + int color, + boolean blockTransfer, + ContainerArcaneCraftingTerminal menu) implements IRecipeTransferError { @Override public Type getType() { - return Type.COSMETIC; + // USER_FACING 阻止转移(allowsTransfer=false)且显示错误;COSMETIC 仅预览 + return blockTransfer ? Type.USER_FACING : Type.COSMETIC; } @Override @@ -195,16 +178,20 @@ public class ThEUseCraftingRecipeTransfer poseStack.pushPose(); poseStack.translate(recipeX, recipeY, 0); - var slotViews = slots.getSlotViews(RecipeIngredientRole.INPUT); - for (int i = 0; i < slotViews.size(); i++) { - var slotView = slotViews.get(i); + // 用 JEI 槽视图 drawHighlight 画高亮(AE2-JEI-Integration 官方做法)。 + // 不能用 menu.slots 的容器坐标:奥术终端槽位顺序与配方格不对应, + // 且 translate(recipeX,recipeY) 后再用相对坐标会双重偏移 → 红框画到屏幕外。 + var inputSlots = slots.getSlotViews(RecipeIngredientRole.INPUT); + for (int i = 0; i < 9; i++) { boolean missing = indices.missingSlots().contains(i); boolean craftable = indices.craftableSlots().contains(i); - if (missing || craftable) { - slotView.drawHighlight( - guiGraphics, - missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR); - } + if (!missing && !craftable) continue; + if (i >= inputSlots.size()) continue; + var slotView = inputSlots.get(i); + slotView.drawHighlight(guiGraphics, + missing ? RED_SLOT_HIGHLIGHT_COLOR : BLUE_SLOT_HIGHLIGHT_COLOR); + ThaumicEnergistics.LOG.info("[JEI-Transfer] showError slot={} missing={} craftable={}", + i, missing, craftable); } poseStack.popPose(); @@ -221,15 +208,15 @@ public class ThEUseCraftingRecipeTransfer int craftable = indices.craftableSlots().size(); if (missing > 0) { tooltip.add(net.minecraft.network.chat.Component.translatable( - "jei.tooltip.transfer.missing", missing)); + "thaumicenergistics.jei.transfer.missing", missing)); } if (craftable > 0) { tooltip.add(net.minecraft.network.chat.Component.translatable( - "jei.tooltip.transfer.craftable", craftable)); + "thaumicenergistics.jei.transfer.craftable", craftable)); } if (craftMissing) { tooltip.add(net.minecraft.network.chat.Component.translatable( - "jei.tooltip.transfer.ctrl.click")); + "thaumicenergistics.jei.transfer.ctrl_click")); } } } diff --git a/src/main/resources/META-INF/neoforge.mods.toml b/src/main/resources/META-INF/neoforge.mods.toml index a5020aa..e76744f 100644 --- a/src/main/resources/META-INF/neoforge.mods.toml +++ b/src/main/resources/META-INF/neoforge.mods.toml @@ -35,7 +35,7 @@ side="BOTH" [[dependencies.thaumicenergistics]] modId="thaumcraft" type="required" -versionRange="[0,)" +versionRange="[0.2.2.95,)" ordering="AFTER" side="BOTH" diff --git a/src/main/resources/assets/ae2/screens/arcane_crafting_terminal.json b/src/main/resources/assets/ae2/screens/arcane_crafting_terminal.json index 1978d9c..795756d 100644 --- a/src/main/resources/assets/ae2/screens/arcane_crafting_terminal.json +++ b/src/main/resources/assets/ae2/screens/arcane_crafting_terminal.json @@ -32,7 +32,16 @@ }, "position": { "left": 8, - "bottom": 177 + "bottom": 179 + } + }, + "player_inventory_title": { + "text": { + "translate": "container.inventory" + }, + "position": { + "left": 8, + "bottom": 94 } } }, diff --git a/src/main/resources/assets/thaumicenergistics/lang/en_us.json b/src/main/resources/assets/thaumicenergistics/lang/en_us.json index 9e03af2..7f4b1eb 100644 --- a/src/main/resources/assets/thaumicenergistics/lang/en_us.json +++ b/src/main/resources/assets/thaumicenergistics/lang/en_us.json @@ -111,5 +111,8 @@ "thaumicenergistics.jade.energy_stored": "Stored: %s / %s", "thaumicenergistics.jade.energy_output": "Output Limit: %s AE/tick", "thaumicenergistics.jade.tank_full": "Tank full, generation stopped", - "thaumicenergistics.gui.distillation_encoder.not_scanned": "Not scanned" + "thaumicenergistics.gui.distillation_encoder.not_scanned": "Not scanned", + "thaumicenergistics.jei.transfer.missing": "Missing: %d", + "thaumicenergistics.jei.transfer.craftable": "Craftable: %d", + "thaumicenergistics.jei.transfer.ctrl_click": "Use Ctrl to craft missing items" } \ No newline at end of file diff --git a/src/main/resources/assets/thaumicenergistics/lang/zh_cn.json b/src/main/resources/assets/thaumicenergistics/lang/zh_cn.json index 674cd61..261440e 100644 --- a/src/main/resources/assets/thaumicenergistics/lang/zh_cn.json +++ b/src/main/resources/assets/thaumicenergistics/lang/zh_cn.json @@ -121,7 +121,7 @@ "tc.research_name.PSEUDO_SCEPTRE": "工艺权杖", "tc.research_name.PSEUDO_COREGATHER": "傀儡核心:聚集", - "thaumicenergistics.jade.suction_type": "吸力: %s", + "thaumicenergistics.jade.current_aspect": "燃料: %s", "thaumicenergistics.jade.essentia_stored": "源质: %d / %d", "thaumicenergistics.jade.burning": "燃烧 %d%%, %.1f AE/tick", "thaumicenergistics.aspect.ignis": "火", @@ -130,5 +130,8 @@ "thaumicenergistics.jade.energy_stored": "已存储: %s / %s", "thaumicenergistics.jade.energy_output": "输出上限: %s AE/tick", "thaumicenergistics.jade.tank_full": "能量槽已满,停止发电", - "thaumicenergistics.gui.distillation_encoder.not_scanned": "未扫描" + "thaumicenergistics.gui.distillation_encoder.not_scanned": "未扫描", + "thaumicenergistics.jei.transfer.missing": "缺少: %d", + "thaumicenergistics.jei.transfer.craftable": "可合成: %d", + "thaumicenergistics.jei.transfer.ctrl_click": "按住 Ctrl 合成缺失物品" } \ No newline at end of file diff --git a/src/main/resources/assets/thaumicenergistics/textures/gui/arcane_crafting.png b/src/main/resources/assets/thaumicenergistics/textures/gui/arcane_crafting.png index 82ea962..a1c8688 100644 Binary files a/src/main/resources/assets/thaumicenergistics/textures/gui/arcane_crafting.png and b/src/main/resources/assets/thaumicenergistics/textures/gui/arcane_crafting.png differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/clay.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/clay.png deleted file mode 100644 index 3e8f4b7..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/clay.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/diamond.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/diamond.png deleted file mode 100644 index 3f7cdf0..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/diamond.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/flesh.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/flesh.png deleted file mode 100644 index 7662a93..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/flesh.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/gold.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/gold.png deleted file mode 100644 index 6c98730..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/gold.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/iron.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/iron.png deleted file mode 100644 index 6c3747b..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/iron.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/stone.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/stone.png deleted file mode 100644 index ea52fce..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/stone.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/straw.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/straw.png deleted file mode 100644 index f2739c2..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/straw.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/tallow.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/tallow.png deleted file mode 100644 index 4d60e21..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/tallow.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/thaum.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/thaum.png deleted file mode 100644 index effad48..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/thaum.png and /dev/null differ diff --git a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/wood.png b/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/wood.png deleted file mode 100644 index 33af719..0000000 Binary files a/src/main/resources/assets/thaumicenergistics/textures/model/golembackpack/wood.png and /dev/null differ diff --git a/src/main/resources/thaumicenergistics.mixins.json b/src/main/resources/thaumicenergistics.mixins.json index 5111ece..7fc6b39 100644 --- a/src/main/resources/thaumicenergistics.mixins.json +++ b/src/main/resources/thaumicenergistics.mixins.json @@ -8,11 +8,13 @@ "ae2.MixinCraftingTreeProcess", "ae2.MixinAbstractTerminalPart", "ae2.MixinConfigMenuInventory", + "ae2.MixinMEStorageMenuAccessors", "tc.MixinAspect", "tc.MixinVisRelayBlock" ], "client": [ "ae2.MixinCraftConfirmScreen", + "ae2.MixinMEStorageScreen", "tc.ResearchPageIndexMixin", "tc.MixinThaumonomiconScreen" ],